Vue的不同构建版本
对于Vue的不同构建版本,以下图说明
- 有min后缀的是生产版本,其他是开发版本
- Full表示完整版,同时包含编译器和运行时
- 编译器:将模板字符串编译为JavaScript渲染函数的代码,即
template => h()
,体积大(3000多行),效率低 - 运行时:用来创建Vue实例,渲染并处理虚拟DOM等代码,不包含编译器,体积小,效率高
- 编译器:将模板字符串编译为JavaScript渲染函数的代码,即
- Runtime-only是运行时版本,不包含编译器
- UMD:通用模块化版本,支持多种模块方式,可以在浏览器环境下直接运行(挂载到window上)
- CommonJS:配合browserify或webpack等打包工具使用
- ES Module:为现代打包工具提供的版本,主要是可以进行静态分析,进行tree-shaking来删除无用代码
vue-cli默认使用vue.runtime.esm.js版本,因为编译的过程实际上放在构建流程中更合适。
- vue-cli对webpack进行了深度封装,如果需要查看webpack的具体配置,可以使用命令
vue inspect > output.js
将webpack的配置输出到output.js文件中,便于查看(但该文件内容不是有效的配置文件,不能直接使用)。 - vue-cli在webpack的resolve选项中添加了
alias: { vue$: 'vue/dist/vue.runtime.esm.js' }
路径别名,这表明使用import vue from 'vue'
时,导入的是vue.runtime.esm.js文件
Vue构造函数及初始化
从UMD标准的完整vue.js版本出发(适用于web开发使用),从该版本的打包入口文件来分析Vue构造函数。
入口文件:src/platforms/web/entry-runtime-with-compiler.js
该入口文件中导入了Vue,循着Vue变量的导入导出轨迹可以得到下面的文件:
src/platforms/web/entry-runtime-with-compiler.js
- web平台打包入口文件
- 重写了平台相关的$mount方法,增加了编译能力。每个平台的$mount都不同,需要重写。
- 注册了Vue.compile静态方法,传递一个HTML字符串,返回render函数
src/platforms/web/runtime/index.js
- web平台相关
- 注册和平台相关的全局指令:v-model、v-show
- 注册和平台相关的全局组件:v-transition、v-transition-group
- 定义了Vue原型的方法:
- __patch__:虚拟DOM转换为真实DOM,就是patch函数
- $mount:挂载方法,内部调用mountComponent
src/core/index.js
core文件夹主要是Vue的初始化global-api文件夹和instance文件夹、响应式处理observer文件夹和虚拟DOM vdom文件夹
- 与平台无关
- 初始化一些实例属性和方法
- 调用initGlobalAPI(Vue),初始化Vue的静态方法
src/core/instance/index.js
- 与平台无关
- 定义了Vue构造函数,调用this._init(options)方法
- 给Vue混入了常用的实例成员$data、$props、 $set、 $delete、 $watch
Vue初始化静态成员
初始化静态成员的操作在src/core/global-api文件夹中。
主要涉及到了以下文件
src/core/global-api/index.js
initGlobalAPI函数:
- 为Vue挂载一个config属性,指向全局配置信息,config见
src/core/config.js
- 为Vue挂载了Vue.set Vue.delete Vue.nextTick三个方法
- 为Vue挂载了Vue.observable方法
- 为Vue挂载了options属性
- options内定义了components、directives和filters三个属性,分别用来存储全局注册的组件、指令和过滤器
- options_base存储了Vue构造函数,内部使用
- options.components初始化了内置组件keep-alive
- 初始化了Vue.use方法
initUse(Vue)
,插件注册src/core/global-api/use.js
- 初始化了Vue.mixin方法
initMixin(Vue)
,全局混入src/core/global-api/mixin.js
- 初始化了Vue.extend方法
initExtend(Vue)
,返回Vue构造函数的子类src/core/global-api/extend.js
- 初始化了Vue.directIve方法、Vue.component方法和Vue.filter方法
initAssetRegisters(Vue)
,全局注册指令、组件和过滤器。src/core/global-api/assets.js
Vue初始化实例成员
初始化实例成员的操作在src/core/instance文件夹中。
src/core/instance/index.js
initMixin(Vue)
:给Vue的原型挂载一个_init()方法stateMixin(Vue)
:注册Vue原型的$data、$props、 $set、 $delete、 $watch这些方法eventsMixin(Vue)
:初始化事件相关方法$on $once $off $emitlifecycleMixin(Vue)
:初始化生命周期相关的混入方法_update(内部调用了__patch__方法来渲染或更新DOM) $forceUpdate $destroyrenderMixin(Vue)
:混入render $nextTick _render
vm._render()方法调用编译后的render函数或用户传入的render函数,返回虚拟DOM节点
vm._update()方法将虚拟DOM节点转换为真实DOM
Vue中使用了with语句来延长作用域链,在作用域链前端增加了临时对象,如with(this){ return _c('div, {attrs: { "id": "app" }}', [_c('h1', [_v(_s(msg))])]) }
,msg变量实际应该是this.msg,通过使用with语句省略了this
数据响应式原理
响应式的代码都定义在src/core/observer文件夹内
将待观察对象传入observe函数,在observe函数中创建Observer对象,Observer构造函数内部创建与待观察对象相关的dep对象,然后遍历待观察对象的属性,使用defineReactive函数创建响应式属性。
每一个被观察对象(如传给Vue的data对象)都会创建一个dep对象,每一个被观察对象的属性也会创建一个dep对象,如果是深度监听,则嵌套对象的属性也会创建dep对象
属性 -> dependencies调用notify -> watcher 异步更新 -> vnode -> views
只有调用notify才会引起视图更新,因此数组元素即使用索引值更新了,也不会触发视图更新,就是因为使用索引值更新没有定义notify,只有调用改写过的数组方法才会调用notify。
依赖收集过程:
- 在getter函数中收集依赖,如果Dep.target存在(即watcher实例化了),则开始收集依赖(将watcher添加到该属性的dep实例的subs属性中)
- 实例化watcher是在mountComponent()函数中,给Dep.target赋值是在watcher的构造函数中,dep.depend是用来将当前Dep.target(即watcher)添加到dep.subs数组中
- watcher用来将更新后的数据转换到虚拟DOM中,并更新真实DOM
数组的监测是用额外的方式实现的,将数组的一些会改变值的方法改写,并添加了dep.notify()方法来触发更新,所以只有使用被改写的数组方法操作数组,采用触发响应式更新。
改写的方法包括’push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’
但是这样的话,只要数组变动,所有依赖数组的watcher都会去更新。
数组的元素也会去收集依赖,但元素更新不会触发响应式,收集依赖的目的是如果元素有变化,则自动更新到新的值
有个问题:为什么数组不能用getter/setter来转换为响应式,毕竟数组也是属性为索引值的对象,而且JavaScript是支持这种响应式写法的
我觉得主要是因为数组的元素数量很多,遍历太多的元素设置响应式会影响性能(内存空间占据大),而且用户一般不会对每个元素都去更改,数组更多的主要是提供遍历,对数组元素内部的嵌套对象只要有监听就行,而不是监听数组的每个元素。
如果要对数组的索引对应的值更新,其实使用改写过的splice方法就可以做到了,splice可以做到删除并插入新元素,实际上就是更新索引对应的值。
Watcher侦听器
Watcher有三种类型,computed watcher(计算属性侦听器)、用户watcher(侦听器属性)、渲染watcher(即实现响应式自动更新视图的watcher)。前两种watcher是在initState方法中初始化。
创建顺序: 计算属性watcher、用户watcher(侦听器)、渲染watcher
用户watcher就是给用户一个接口,用于自定义数据更新时的回调函数,类似Hook机制。
渲染watch的创建:
渲染watcher在mountComponent中创建,mountComponent是在编译模板后调用,只要执行mountComponent挂载到DOM中时,就会创建渲染watcher。在代码中,通常就是创建Vue实例或者组件实例时会创建渲染watcher。
- 渲染watcher的getter属性是updateComponent函数
计算属性watcher和用户watcher的创建:
在vue/src/core/instance/init.js
文件中,Vue实例方法_init中调用initState()方法(vue/src/core/instance/state.js
),该方法内部初始化了实例状态,即将选项对象的data、props、methods、computed、watch等属性初始化,并创建了计算属性watcher和用户watcher,存入vm._watchers数组中。
- 计算属性watcher和用户watcher的getter属性分别是计算属性的getter函数和用户watcher侦听的数据的getter函数。
计算属性的初始化与watcher的lazy属性缓存效果
在创建好计算属性watcher后,调用defineComputed(vm, key, userDef)
将计算属性挂载到vm上。在挂载的过程中,defineComputed(vm, key, userDef)
会调用createComputedGetter(key)
,为计算属性创建getter。如果计算属性也定义了setter,则同时也会创建setter。
重点是计算属性的getter如何实现缓存
function createComputedGetter (key) {
// 返回了计算属性的getter函数
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
计算属性的watcher.lazy是true, 则watcher.dirty = watcher.lazy = true
,就会执行watcher.evaluate()
,从而求得watcher.value值作为getter返回的值,并且将watcher.dirty = false
。另外,当我们执行this.get()
时是会为Dep.target赋值的,所以还会执行watcher.depend()
,将计算属性的watcher添加到依赖中去。现在,计算属性的初始化完毕。只要watcher.dirty为false,则计算属性的值就不会重新计算,实现了缓存效果。
除非计算属性的依赖data发生变化,则会引起watcher.update执行,重新对watcher的dirty赋值为true,计算属性重新求值。
update () {
/* istanbul ignore else */
if (this.lazy) {
// 计算属性的watcher,dirty属性赋值true
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 将当前watcher放入队列,异步执行更新DOM的操作
queueWatcher(this)
}
}
watcher的执行过程
- 由dep.notify()方法触发,遍历dep中的watcher,按顺序依次执行watcher.update
- watcher.update中,将watcher放入队列中合适的位置(按watcher的id来排序),等待异步调用
nextTick(flushSchedulerQueue)
- 触发beforeUpdate钩子函数
- watcher调用run方法
- 执行计算属性watcher和用户watcher
- 执行渲染watcher,更新Vnode,更新DOM
Vue.set静态方法或vm.$set方法
- 对于数组的处理,可以使用
vm.$set(vm.items, indexOfItem, newValue)
使用索引指定值,同样是响应式,原因在于内部使用了splice方法 - 对于对象的处理,如果对象是响应式的,则将属性也设置为该对象的响应式属性,并且调用notify(表示对象有更新),更新视图。
Vue.delete或vm.$delete方法
与Vue.set类似,使用splice操作数组、使用delete操作符删除对象属性,然后调用notify。
Vue监测不到的那些操作主要是由于这些操作并没有增加notify通知更新视图,但实际上这些操作已经改变了数据,只是没有反映到视图上。
对象内部属性值的更改不会触发对象的更改(即不会触发对象发出更新通知notify),而给对象新增或删除、或者重置该对象的值则会触发对象的更改。
比如使用Vue.$watch()(用户watcher)浅监听对象时,更改对象内部的属性值并不会触发对象的notify。
Vue.nextTick(cb):将回调函数cb放入异步更新的队列中,从而获取到DOM更新后的值。
在watcher更新DOM时,同样在watcher的update方法中使用Vue.nextTick将watcher放入异步队列,然后异步更新DOM。因此,DOM的更新都是异步的。
首次渲染
过程
初始化 -> 编译 -> 挂载
其中挂载又包括:实例化watcher -> 创建Vnode(在此期间收集依赖) -> 渲染Vnode到DOM
Vue() -> this._init():初始化了很多事情,如生命周期、事件、响应式数据、触发beforeCreate钩子函数、依赖注入、状态、触发created钩子函数 -> 调用平台相关的this.$mount():被重写后的$mount -> 编译模板:调用将模板编译为render函数的compileToFunctions -> 调用与平台无关的vm.$mount方法 -> 调用mountComponent():内部定义了updateComponent方法 ->beforeMount钩子函数触发 -> 实例化new Watcher(): 渲染watcher -> updateComponent() -> vm._render() -> 渲染函数render -> createElement(): 创建vnode -> vm._update() -> vm.__patch__():将Vnode转换到真实DOM树
Vue的虚拟DOM
h函数:即vm.$createElement的别名
参数
- tag
- data
- attrs:特性节点
- props:属性
- …
- children
- text
- elm:虚拟DOM节点转换为真实DOM节点后存储到elm,vnode与DOM节点的映射关系,在将vnode与DOM树操作时要用到
- key
updateComponent函数在mountComponent中定义,然后传递给Watcher构造函数,在watcher中执行。
updateComponent函数先调用vm._render函数
虚拟DOM的创建流程:
watcher -> updateComponent -> _update -> _render -> render(用户传入或者模板编译得到的render) -> vm.$createElement或vm._c(_c表示模板编译的render) -> createElement -> _createElement:创建并返回Vnode
- createElement:用来对vm.$createElement传入的参数进行处理,再转交给_createElement。也就是说,createElement函数是用来保证API的一致性。
- _createElement:创建vnode
- 不允许vnode的data使用响应式数据,否则调用new Vnode,返回空节点
- data.is属性:用于创建动态组件
- data.key属性:用来提高虚拟DOM比对的效率,重用DOM节点
- Vnode构造函数:类似于snabbdom中的Vnode,但提供了更加丰富的属性
- _update方法:处理vnode
-
调用vm.__patch__()方法
- 首次渲染:将vm.$el转化为vnode,比较差异,挂载到真实DOM
- 更新渲染:比较前后vnode的差异,更新到真实DOM
-
vm.__patch__()实际上就是patch函数(
src/platforms/web/runtime/patch.js
),由createPatchFunction({ nodeOps, modules })
生成- nodeOps是一些DOM节点操作方法的集合
- modules是platformModules和baseModules的拼接集合
- platformModules是平台相关的API方法,在web中是attrs, klass, events, domProps, style, transition,操作DOM的其他节点或事件
- baseModules是与平台无法的API方法,这里是directive和ref
- patch函数初始化:收集modules中的hooks函数
- patch函数的执行:
- 如果只调用vm.$mount(),不传入挂载点元素,则不挂载到DOM,只创建该DOM节点
- 判断oldVnode.nodeType
- 如果是1则为真实DOM,说明是首次渲染
- 将oldVnode转换为Vnode,调用了emptyNodeAt(elm)
- 找到oldVnode的父元素,将新的Vnode创建为DOM节点并插入到父元素之中。
- 如果不是1,则开始判断前后两个节点是否相同
- 如果相同,则开始执行patchVnode,对比两个节点并更新DOM
- 如果不相同,则移除旧节点,挂载新节点
- 如果是1则为真实DOM,说明是首次渲染
- 触发insertedVnodeQueue中所有vnode的insert钩子函数,insert钩子函数定义在vnode.data.hook.insert上
- createElm函数:将vnode转换为DOM元素节点并插入到DOM树
- vnode是组件、标签元素、注释节点、文本节点四种情况分别处理
- createChildren函数
- checkDuplicateKeys:判断是否有相同的key
- 通过createElm创建DOM元素节点或创建文本节点
- 触发create钩子函数
- 先触发modules中的create钩子函数
- 再触发data.hook.create钩子函数
- 判断是否有insert钩子函数,如果有,则将vnode加入到insertedVnodeQueue数组中
- 执行insert,将DOM元素插入到DOM树中
-
patchVnode函数的执行
- 执行prepatch钩子函数
- 获取新旧节点的子节点集合
- 调用modules中的update钩子函数,更新节点的属性、样式、事件等等
- 调用用户传入的update钩子函数
- 开始对比新旧两个节点
- 新的节点没有text
- 如果新旧节点都有子节点,且子节点不同,则调用updateChildren
- updateChildren内部基本上与snabbdom的方式一样,但在使用key的时候有点差别。Vue在使用新节点的key在旧节点中查找(这时候的查找使用的是key-index的映射表)时,如果没有对应的索引,则会遍历旧节点来找到。如果还没找到,才认定新节点需要创建,而不能复用旧节点。
- 如果新节点有子节点,旧节点没有子节点,则调用checkDuplicate判断key是否有重复,然后情况旧节点的文本内容,调用addVnodes把新节点的子节点添加到DOM树
- 如果旧节点有子节点,新节点没有子节点,则调用removeVnodes删除旧节点的子节点,触发remove和destroy钩子函数
- 如果新旧节点都没有子节点,但旧节点有text,清空旧节点的text内容
- 如果新旧节点都有子节点,且子节点不同,则调用updateChildren
- 新旧节点的text不一致,更新text内容
- 新的节点没有text
- 获取postpatch钩子函数并执行
-
Vue的模板编译
模板编译为render函数
模板编译开始的位置:web平台下的重写之后的Vue.prototype.$mount()中调用
模板编译的结果
- 对template编译后得到vm._c、vm._m、vm._v等函数,在Vue中使用了
with(this)
语法来扩展作用域链,从而使得调用_c、_m等不需要在前面添加vm。 - 这些函数除了_c外,都定义在
src/core/instance/render-helpers
中,在这个文件中,导出了installRenderHelpers
,该函数给vm挂载了很多render辅助方法- 在
src/core/instance/render.js
中导入了installRenderHelpers
,在renderMixin
函数中调用installRenderHelpers(Vue.prototype)
- _m:即
renderStatic
,处理模板中的静态内容 - _v:即createTextVnode,创建文本节点
new VNode(undefined, undefined, undefined, String(val))
。标签中的空格、换行的文本内容也会创建文本节点,而这些文本内容一般是没意义的,所以尽量不要有这些内容。Vue3中的render函数已经为此优化过了。 - _s:即toString,主要使用
JSON.stringify()
或String()
来将参数转换为字符串
- 在
- _c函数在
src/core/instance/render.js
中,定义为vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
- 对于组件而言,组件标签并没有进一步展开对组件模板的编译,组件模板的编译开始于调用createElement函数,即render函数调用,将标签转换为vnode,其中调用了createElement函数。
vue-template-explorer
使用vue-template-explorer来将模板编译为render函数
模板编译的过程
-
在web平台,入口文件
src/platforms/web/entry-runtime-with-compiler.js
中为vue实例注册了compileToFunctionsVue.compile = compileToFunctions
-
在
src/platforms/web/compiler/index.js
中,调用createCompiler(baseOptions)
返回compileToFunctions,其中baseOptions如下:// baseOptions定义 baseOptions: CompilerOptions = { expectHTML: true, modules, directives, isPreTag, isUnaryTag, mustUseProp, canBeLeftOpenTag, isReservedTag, getTagNamespace, staticKeys: genStaticKeys(modules) }
-
createCompiler函数又是在文件
src/compiler/index.js
中,由createCompilerCreator函数接收参数baseCompile而创建,createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) {}
-
createCompiler函数定义了compile函数,并导出该函数
-
createCompiler函数导出compileToFunctions
// createCompiler函数的返回值 return { compile, compileToFunctions: createCompileToFunctionFn(compile) }
-
-
createCompilerCreator函数在
src/compiler/create-compiler.js
中定义 -
整个调用流程为createCompilerCreator -> createCompiler -> createCompileToFunctionFn(compile) -> compileToFunctions -> Vue.compile
-
CompileToFunctions函数:
-
先判断是否有编译缓存
-
调用compile(template, options),返回编译结果compiled。
- compiled.render内容是JavaScript代码的字符串形式
-
调用
createFunction(compiled.render, fnGenErrors)
,将字符串形式的JavaScript代码转换为函数,内部很简单,使用了new Function的方式res.render = createFunction(compiled.render, fnGenErrors) res.staticRenderFns = compiled.staticRenderFns.map(code => { return createFunction(code, fnGenErrors) })
-
缓存编译结果,以template为key,以res对象为值
-
编译的核心函数compile
compile (template: string, options?: CompilerOptions)
,如果传入了options,则合并baseOptions与options为finalOptions- 调用baseCompile(template.trim(), finalOptions),返回compiled(字符串形式的JavaScript代码),并记录编译的错误信息。
baseCompile模板核心编译函数
-
模板转换为抽象语法树
ast = parse(template.trim(), options)
- 模板字符串转换成AST后,可以通过AST做优化
- 标记模板中的静态内容,在patch时可以直接跳过(静态内容不会变化,不用比对和重新渲染)
-
优化抽象语法树
optimize(ast, options)
-
将抽象语法树转换为字符串形式的JavaScript代码
code = generate(ast, options)
-
返回编译结果对象
return { ast, // 渲染函数,不过要注意,这里的render并不是我们最终结果的render函数 render: code.render, // 静态渲染函数,生成静态Vnode树 staticRenderFns: code.staticRenderFns }
使用AST explorer网站查看各种语言或工具生成的AST
使用节点属性static来表征节点是否是静态的。
parse函数解析模板字符串生成AST
在src/compiler/parser/index.js
文件中定义了parse函数以及一系列解析指令、修饰符等的函数
- 调用parseHTML函数:接收template,返回root(AST),内部会调用createASTElement()创建AST节点
- 通过options传入了一些解析中需要用到的函数如下
- start:解析到开始标签时执行,创建AST节点,解析指令
- end:解析到结束标签时执行
- chars:解析到文本内容时执行
- comment:解析到注释内容时执行
- parseHTML函数在
src/compiler/parser/html-parser.js
中定义,借鉴了http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
开源库- 通过预定义一些正则表达式,开始对html解析,每解析完一次,就将html截取未解析的部分后重新复制。
- 解析指令:通过指令名称,获取指令的值,然后记录到AST节点中
- 通过options传入了一些解析中需要用到的函数如下
优化AST
optimize(ast, options)
在文件src/compiler/optimizer.js
中定义
markStatic(root)
标记root中的静态节点isStatic(node)
判断是否静态节点,记录到node.static中- 如果有node.pre(由v-pre指令而来)属性,也是静态的
- 判断是否是保留标签,如果不是,则是组件,需要对判断tag是不是slot以及inline-template
- 遍历子节点,递归调用markStatic,如果子节点只要不是static,那节点就不是static
- 判断条件渲染的节点,即node.ifConditions,遍历条件渲染值,标记静态节点。
markStaticRoots(root, false)
标记root中的静态根节点。静态根节点指的是节点有子节点集合(大于1),且第一个子节点不是文本节点,实际上就是元素嵌套元素时,且元素内容是文本的形式。- 判断过程与markStatic类似
生成代码
code = generate(ast, options)
在文件src/compiler/codegen/index.js
中定义。
-
const state = new CodegenState(options)
创建生成代码过程中用到的状态对象 -
code = ast ? genElement(ast, state) : '_c("div")'
调用genElement(ast, state)
生成代码 -
返回结果对象
return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns }
-
genElement的执行:代码全部都是字符串形式
-
判断节点是否是静态根节点且没有被处理过,如果是,则调用
genStatic(el, state)
- genStatic(el, state)内部再次调用genElement方法,将生成的代码拼接后push到state.staticRenderFns中。因为一个AST中可能会有多个静态子树,所以将这些静态子树以数组形式存储。返回一个字符串形式的代码
state.staticRenderFns.push(`with(this){return ${ genElement(el, state)} }`) // ...中间的代码略过 // 返回的字符串形式的代码,该代码只记录静态子树渲染函数代码的索引,而实际代码在数组中,通过_m()函数来得到 return \`_m(${ // 这里实际上就是每个静态子树对应的索引 state.staticRenderFns.length - 1 }${ el.staticInFor? ',true' : '' })`
- _m方法:就是renderStatic函数,内部由一个cached缓存,存储的是静态子树索引与对应的Vnode子树(如果缓存没有,则调用静态子树的渲染函数来创建Vnode子树,并调用markStatic()标记该子树的静态属性为true)
tree = cached[index] = this.$options.staticRenderFns[index].call( this._renderProxy, null, this // for render fns generated for functional component templates ) // 标记静态子树的静态属性为true markStatic(tree, `__static__${index}`, false)
-
getData函数返回一个字符串形式的data对象,该data对象用于传入_createElement函数来创建Vnode
-
处理子节点,调用genChildren将子节点转换为数组形式,genChildren中对children调用map方法,对children中的每个元素调用gen方法将其生成一个节点对应的代码
-
将生成的代码组合起来,从AST节点到子节点的代码
-
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
createCompileToFunctionFn(compile)将compile函数返回的compiled对象内部的字符串形式JavaScript转换为函数形式的JavaScript代码
- 调用createFunction将字符串形式的code转换为函数
new Function(code)
// 把字符串形式代码转换为函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
模板编译总结
模板字符串 -> AST -> 优化(标记静态节点) -> 生成字符串形式代码 -> new Function(code)匿名函数
Vue的组件
- 一个组件就是一个拥有预定义选项的Vue实例
- 一个组件可以组成页面上的一个功能完备的区域,组件可以包含脚本、样式、模板
组件注册
-
全局注册时,会将传入的选项对象使用Vue.extend(options)转换为Vue的子类构造函数,再记录;如果传入的是函数,则直接记录在Vue.options[‘components’]对象中。
-
局部注册时,先不会转换为构造函数,而是将组件的选项对象记录在父组件的options[‘components’]属性中。
Vue.extend方法
在src/core/global-api/extend.js
文件中,Vue.extend方法定义在函数initExtend中
- 设定构造函数的cid属性(Vue和组件构造函数每个都具有cid),用于缓存
- 组件构造函数与Vue形成继承关系
- 使用proxy getters初始化props和computed属性
- 将Vue的静态成员赋值给组件构造函数
组件创建
在编译模板之后,模板内部的组件对应标签并未被进一步编译,组件的创建开始于模板编译过的render函数被调用,render函数内部调用了createElement函数,组件的创建开始于createElement函数,然后就像父组件或根Vue节点一样,开始编译组件自己的模板。
- createElement创建占位vnode:在创建组件对应的vnode时,会先去父组件注册的组件里寻找,找不到的时候回退到原型链上寻找(即寻找全局组件)。找到后,再调用
createComponent()
,根据选项对象生成构造函数,进而创建组件,先生成一个占位用的vnode - patch过程中调用init钩子函数生成组件实例:接下来在新老vnode的对比中,调用了init钩子函数创建真正的组件实例与对应的vnode,重新走一遍Vue实例的创建、编译模板、挂载流程。
组件构造函数或组件选项对象传入render的h函数(vm.$createElement
)中,在init钩子函数中转换为vnode并返回。
组件的颗粒度不是越小越好,因为组件的创建过程会多次执行,比较耗费性能。
_createElement()
-> src/core/vdom/create-component.js
文件中的createComponent():创建并返回vnode -> installComponentHooks -> new Vnode()
-> 调用patch()函数 -> createElm -> src/core/vdom/patch.js
文件中的createComponent -> init -> createComponentInstanceForVnode -> new vnode.componentOptions.Ctor(options)
调用组件构造函数创建实例 -> 调用this._init(),即Vue的实例方法_init -> 调用组件的$mount(),挂载到DOM树上。
createComponent:安装钩子函数,创建占位Vnode
-
第一个参数Ctor: Class<Component> | Function | Object | void,当传入的是对象时,调用
Ctor = baseCtor.extend(Ctor)
Vue.extend来将对象转换为构造函数 -
调用installComponentHooks安装组件钩子函数,将用户传入的钩子函数与componentVNodeHooks中的四个钩子函数(init、prepatch、insert和destroy)做合并。
-
合并的过程就是将两个同类的钩子函数放在一个函数中,依次调用
function mergeHook (f1: any, f2: any): Function { const merged = (a, b) => { // flow complains about extra args which is why we use any f1(a, b) f2(a, b) } merged._merged = true return merged }
-
组件实例的创建过程来自于init钩子函数,init内部调用createComponentInstanceForVnode,而createComponentInstanceForVnode调用
new vnode.componentOptions.Ctor(options)
,其中的Ctor即组件构造函数
-
-
调用
new Vnode
创建Vnodeconst vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory )
patch过程创建真正的组件实例与对应Vnode
init钩子函数创建组件实例
init钩子函数在patch函数中调用
patch函数将组件对应的vnode转换到真实DOM上
patch -> createElm -> src/core/vdom/patch.js
文件中的createComponent -> init -> createComponentInstanceForVnode -> 调用组件构造函数 -> 调用this._init(),即Vue的实例方法_init -> 调用组件的$mount(),开始挂载过程(包括创建vnode、依赖收集、patch过程转换为真实DOM)。
// createComponent中获取init钩子函数并调用
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
_init方法中对组件执行的方法
- initInternalComponent
- initLifecycle,记录组件的父子关系
- Vue.prototype._update中执行
const restoreActiveInstance = setActiveInstance(vm)
,将组件实例缓存。调用restoreActiveInstance()
恢复activeInstance。这些代码为了解决组件嵌套时,记录父子组件的关系。 - 子组件没有el选项,所以不执行挂载。
if (vm.$options.el) {vm.$mount(vm.$options.el)}
组件更新
组件中的data数据或者props变更时,会导致组件的更新。此时,组件对应的watcher会调用getter(即updateComponent函数)开始对组件重新创建vnode并返回。
类组件的更新只是会调用当前组件实例的render方法来重新创建Vnode
函数式组件的更新会卸载当前组件,并重新创建新组件,因为函数式组件没有组件实例,不包括生命周期函数。因此,函数是组件的更新包含了两个过程:卸载–创建。