当前位置: 首页 > Web前端 > HTML5

深入Vue.js从源码开始(一)

时间:2023-04-05 11:14:24 HTML5

本系列是MOOC《Vue.js 源码全方位深入解析》课程UnderstandingFlowFlow的学习笔记Flow是facebook出品的JavaScript静态类型检查工具。Vue.js的源代码使用Flow进行静态类型检查。为什么静态类型检查JavaScript是一种动态类型的语言,但是由于是弱类型,往往在编译阶段很难发现这些隐患,运行时就会出现各种bug。类型检查是当前动态类型语言的发展趋势。可以在编译期间尽早发现错误(由类型错误引起)。越早在上游发现,越有利于项目的成本控制。Vue为什么选择FlowVue.js在做2.0重构的时候,在ES2015的基础上,除了ESLint保证代码风格外,还引入了Flow进行静态类型检查。之所以选择Flow,主要是因为Babel和ESLint都有相应的Flow插件来支持语法,可以完全遵循现有的构建配置,并且可以具有静态类型检查的能力,成本变化非常小。Flow的安装npminstall-gflow-binFlow通常有两种工作方式:类型推断:通过变量的使用上下文推断变量类型,然后根据这些推断来检查类型。类型注解:预先注解我们期望的类型,Flow会根据这些注解进行判断。typeinferencefirstcode/*@flow*/``functionsplit(str){returnstr.split('')}split(11)Flow检查上面代码后会报错,因为函数split期望的参数是一个字符串,我们输入了数字。错误┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈│││││││││││index.js:4:14无法调用str.split,因为Number[1]中缺少属性split。1│/*@flow*/``2│3│functionsplit(str){4│returnstr.split('')5│}6│[1]7│split(11)Found1error案例中oftypeannotation类型推断,下面的代码不会报错/*@flow*/functionadd(x,y){returnx+y}add('Hello',11)这往往是隐患。flow的正确写法应该是/*@flow*/functionadd(x:number,y:number):number{returnx+y}add('Hello',11)flow的具体用法可以参考到官网flowg官网链接Flow在Vue.js源码中的应用Flow提出了一个libdef的概念,可以用来识别这些第三方库或者是自定义类型,而Vue.js也利用了这个特性.在Vue.js的主目录下有一个.flowconfig文件,就是Flow的配置文件。[libs]部分用于描述包含指定库定义的目录。默认是名为flow-typed的目录。这里[libs]配置的是flow,也就是说指定的库定义都在flow文件夹下。当我们打开这个目录时,我们会发现以下文件:flow├──compiler.js#编译相关├──component.js#组件数据结构├──global-api.js#全局API结构├──modules。js#No.三方库定义├──options.js#选项相关├──ssr.js#服务端渲染相关├──vnode.js#虚拟节点相关源代码稍后。Vue.js源码目录设计Vue.js的源码在src目录下,其目录结构如下src├──compiler#编译相关├──core#核心代码├──platforms#支持不同platforms├──server#服务端渲染├──sfc#.vue文件解析├──shared#共享代码编译器编译器目录包含了所有Vue.js编译相关的代码。包括将模板解析成ast语法树、ast语法树优化、代码生成等功能。可以在构建时进行编译(借助webpack、vue-loader等辅助插件);它也可以在运行时完成,使用包含构建函数的Vue.js。我们在开发的时候经常使用runtime编译,而offline经常用于release。ast语法树介绍一目了然JS抽象语法树核心目录包含了Vue.js的核心代码,包括内置组件、全局API封装、Vue实例化、观察者、虚拟DOM、实用函数等。platformVue.js是一个跨平台的MVVM框架,它可以运行在web上,也可以用weex运行在natvie客户端上。platform是Vue.js的入口,两个目录代表两个主要入口,分别打包成运行在web端的Vue.js和weex。serverVue.js2.0支持服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。服务器端渲染的主要工作是将组件渲染成服务器端的HTML字符串,直接发送给浏览器,最后将静态标记“混合”成客户端上的完全交互的应用程序。sfc通常我们开发Vue.js都是借助webpack来构建,然后通过.vue单文件来编写组件。该目录中的代码逻辑会将.vue文件的内容解析为JavaScript对象。sharedVue.js会定义一些工具方法,这里定义的工具方法会被浏览器端的Vue.js和服务端的Vue.js共享。Vue.js源码构建Vue.js源码是基于Rollup构建的,其构建相关的配置都在scripts目录下。ps:roolup常用于构建纯js库,而webpack更强大,可以将css和图片一起打包到package中。Roolup显然更适合这里。构建脚本通常一个基于NPM的项目都会有一个package.json文件,它是一个项目的描述文件,它的内容其实是一个标准的JSON对象。我们通常将script字段配置为npm的执行脚本。Vue.js源码构建的脚本如下:{"script":{"build":"nodescripts/build.js","build:ssr":"npmrunbuild--web-runtime-cjs,web-server-renderer","build:weex":"npmrunbuild--weex"}}当在命令行运行npmrunbuild时,实际上会执行nodescripts/build.js构建过程我们分析一下构建过程根据源码,首先打开构建入口js文件,在scripts/build.js中:letbuilds=require('./config').getAllBuilds()//通过命令行过滤构建argif(process.argv[2]){constfilters=process.argv[2].split(',')builds=builds.filter(b=>{returnfilters.some(f=>b.output.file.indexOf(f)>-1||b._name.indexOf(f)>-1)})}else{//默认过滤掉weex构建builds=builds.filter(b=>{returnb.output.file.indexOf('weex')===-1})}build(builds)这段代码的逻辑很简单,先从配置文件中读取配置,然后通过命令行参数过滤构建配置,这样就可以构建Vue.js为了不同的目的。接下来让我们看一下配置文件,在scripts/config.js中constbuilds={//Runtimeonly(CommonJS)。由捆绑器使用,例如Webpack&Browserify'web-runtime-cjs':{entry:resolve('web/entry-runtime.js'),dest:resolve('dist/vue.runtime.common.js'),格式:'cjs',banner},//...//运行时+编译器开发构建(浏览器)'web-full-dev':{entry:resolve('web/entry-runtime-with-compiler.js'),dest:resolve('dist/vue.js'),format:'umd',env:'development',alias:{he:'./entity-decoder'},banner},//Runtime+compilerproductionbuild(Browser)'web-full-prod':{entry:resolve('web/entry-runtime-with-compiler.js'),dest:resolve('dist/vue.min.js'),格式:'umd',env:'production',alias:{he:'./entity-decoder'},banner},//...}对于单个配置,它遵循Rollup的构建规则。entry属性表示构建入口JS文件的地址,dest属性表示构建JS文件的地址。format属性表示构建的格式,cjs表示构建的文件遵循CommonJS规范,es表示构建的文件遵循ESModule规范。umd表示构建的文件遵循UMD规范。ps:什么是cjs、es、umd以上面的web-runtime-cjs配置为例,它的入口是resolve('web/entry-runtime.js'),我们看一下resolve函数的定义第一的。源码目录:scripts/config.jsconstaliases=require('./alias')constresolve=p=>{constbase=p.split('/')[0]if(aliases[base]){返回路径.resolve(aliases[base],p.slice(base.length+1))}else{returnpath.resolve(__dirname,'../',p)}}11这里resolve函数的实现很简单,它首先把resolve函数传入的参数p通过/拆分成一个数组,然后将数组的第一个元素设置为base。在我们的例子中,参数p是web/entry-runtime.js,那么base就是web。base并不是实际路径,它的真实路径依赖于alias的配置,我们看一下alias配置的代码,在scripts/alias中:constpath=require('path')module.exports={vue:小路。resolve(__dirname,'../src/platforms/web/entry-runtime-with-compiler'),编译器:path.resolve(__dirname,'../src/compiler'),核心:path.resolve(__dirname,'../src/core'),shared:path.resolve(__dirname,'../src/shared'),web:path.resolve(__dirname,'../src/platforms/web'),weex:path.resolve(__dirname,'../src/platforms/weex'),服务器:path.resolve(__dirname,'../src/server'),条目:path.resolve(__dirname,'../src/entries'),sfc:path.resolve(__dirname,'../src/sfc')}这里web对应的真实路径是path.resolve(__dirname,'../src/platforms/web'),这个path我找到了Vue.js源代码的web目录。然后resolve函数通过path.resolve(aliases[base],p.slice(base.length+1))找到最终路径,也就是Vue.js源码web目录下的entry-runtime.js。于是找到了web-runtime-cjs配置对应的入口文件。通过Rollup构建打包后,最终会在dist目录下生成vue.runtime.common.js。RuntimeOnlyVSRuntime+Compiler通常我们在使用vue-cli初始化我们的Vue.js项目时,会询问我们是使用RuntimeOnly版本还是Runtime+Compiler版本。让我们比较下面的两个版本。?RuntimeOnly当我们使用RuntimeOnly版本的Vue.js时,通常需要使用webpack等vue-loader工具将.vue文件编译成JavaScript,因为是在编译阶段完成的,所以只包含Vue在运行时是.js代码,所以代码体积会更轻。?Runtime+Compiler如果我们不预编译代码,而是使用Vue的template属性,传入一个字符串,则需要在客户端编译模板,如下所示://requirestheversionofthecompilernewVue({template:'

{{hi}}
'})//这种情况不需要newVue({render(h){returnh('div',this.hi)}})因为在Vue.js2.0中,最终的渲染是通过render函数。如果写了template属性,需要编译成render函数。那么这个编译过程会在运行时发生,所以需要有编译器的版本。显然,这个编译过程在性能上会有一定的损失,所以我们通常推荐使用Runtime-OnlyVue.js。