从_init
函数出发可以慢慢追溯到整个Vue
的生命周期,这篇文章主要是记录每一步大致都干了些什么事。
先直接看看_init
的代码:
1 | Vue.prototype._init = function(options?: Object) { |
上面的代码基本都是来操作 vm 的,给它添加各种成员,光在脑海里想太抽象了,最好拿一个简单的例子跑一下,然后每一步打断点看看 vm 的变化。例如如下的简单代码:
1 | <div id="app"> |
resolveConstructorOptions
用于合并构造器及构造器父级上定义的options
。
1 | function resolveConstructorOptions(Ctor: Class<Component>) { |
我们的测试代码中没有父构造器,这里直接返回Ctor.options
。打印一下它:
1 | { |
随后调用mergeOptions
之后,会把我们new Vue
是传入的选项合并进去,打印一下 merge 之后的结果:
1 | { |
可以看到添加了 2 个属性,正好是我们传给 Vue 的成员。
initLifecycle
给 vm 添加$parent
、$root
、$children
、$refs
等属性。
1 | function initLifecycle(vm: Component) { |
抽象组件比如keep-alive
、transition
等,所有的子组件$root
都指向顶级组件.
initEvents
初始化事件相关的属性,_parentListeners
是父组件中绑定在自定义标签上的事件,供子组件处理。
1 | function initEvents(vm: Component) { |
我们暂时还没有添加监听器,所以上面的处理会被跳过。
initRender
给 vm 添加$slots
、$scopeSlots
、_c
、$createElement
、$attrs
、$listeners
等属性;
1 | function initRender(vm: Component) { |
_c
和createElement
是与虚拟 dom 相关的,$slots
和$scopedSlots
和 slot 相关,他们的具体逻辑先略过。
其中$attrs
和$listeners
通过defineReactive
设置.
defineReactive
它就是 Vue 的响应式核心代码,用到了我们熟知的Object.defineProperty
来重写属性值的getter
和setter
。 具体逻辑见代码注释。
1 | function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) { |
核心逻辑还是利用 Dep 和 Watcher 这两个类来收集依赖,同时用到了观察者设计模式。
callHook
用来调用 vm 特定的生命周期钩子。
1 | export function callHook(vm: Component, hook: string) { |
上面的vm.$options[hook]
是在什么时候被赋值的呢? 其实也是在_init
的
1 | vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm); |
比如我们设置了beforeCreate
钩子,那么options
中就会有这个成员函数。那么生命周期钩子的合并策略是怎样的呢? 在options.js
中已经定义好了:
1 | /** |
可以很容易看出来所有同名钩子回调都会合并到一个数组中。
initInjections、initProvide
用于解析inject
和provide
选项的,这俩可能有些人不知道,在官网教程中有描述.
1 | export function initProvide(vm: Component) { |
resolveInject
用于归一化inject
,并找到每个inject
成员在祖先组件中对应的provide
。最终每个解析到的inject
都是直接放到 vm 上。
initState
处理props
、methods
、data
、computed
、watch
,将他们统统挂载到 vm 上.
1 | function initState(vm: Component) { |
这里对数据的操作很多,如果没有耐心,可直接跳过看看处理完之后 vm 的变化,在我们的例子中,我列出了一部分:
1 | { |
\$mount
经过我们上篇文章,我们已经知道,vm.$mount
真正的定义是在entry-runtime-with-compiler.js
中。
1 | const mount = Vue.prototype.$mount; |
compileToFunctions
他就是 Vue 模板编译器的入口了,完成之后返回的render
和staticRenderFns
,前者用于生成 vnode,后者专门用于渲染纯静态节点。
compileToFunctions
的生成逻辑有点绕,核心处理逻辑最后是在src/compiler/index.js
,含有 3 个核心步骤:
1 | // `createCompilerCreator` allows creating compilers that use alternative |
每一步的具体逻辑我们以后的文章专门再讲,逻辑很多。不过我们可以先打断点看看每一步生成的结果,为了生成一个 staticRenderFns,需要有纯静态节点,我们将测试代码template
稍作修改:
1 | <div id="app"> |
第一步 parse 生成的 AST 为:
1 | var ast = { |
第二步 optimize 之后的结果:
1 | var ast = { |
主要是给各种 ast 节点添加上static
、staticRoot
、staticInFor
以标记是否为静态 ast,文字和纯静态 dom 节点的static
都为 true。
第三步 generate 生成的 render 和 staticRenderFns 为:
1 | var render = ` |
注意render
和staticRenderFns
的内容在这里都还只是字符串形式,经过compileToFunctions
的处理会变成真正的匿名函数。
_c
是(a, b, c, d) => createElement(vm, a, b, c, d, false)
。
我们简单说一下createElement
干了什么。a
是要创建的标签名,这里是div
。接着b
是data
,也就是模板解析时,添加到div
上的属性等。c
是子元素数组,所以这里又调用了_c
来创建一个p
标签。
_v
是createTextVNode
,也就是创建一个文本结点。_s
是_toString
,也就是把message
转换为字符串,在这里,因为有with(this)
,所以message
传入的就是我们data
中定义的第一个vue
实例。
mount
拿到render
和staticRenderFns
后,会调用mount
,它的真正代码放在了src/core/instance/lifecycle.js
的mountComponent
中:
1 | function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component { |
大体做了几件事:
- 调用
beforeMount
钩子 - 调用
vm._render
生成 vnode - 调用
vm._update
,传入 vnode - 调用
vm.__patch__
- 调用
mounted
钩子
Watcher
看到在mountComponent
中还生成了一个Watcher
对象,会绑定在 vm._watcher 上。 Watcher 的构造函数看看长什么样:
1 | constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) { |
最后的this.get()
会把我们在mountComponent
中会把传入的updateComponent
调用一次,这样我们的_update
和_render
才会被执行。
vm._render
会利用我们的 render 函数生成 vnode:
1 | Vue.prototype._render = function(): VNode { |
可以看到核心的render.call(vm._renderProxy, vm.$createElement)
,跟我们自己写render
很像,也是有一个$createElement
参数。
vm._update
在这一步之前,页面的 dom 还没有真正渲染.
如果是初始化,则会把 vnode 渲染到页面中;如果是页面更新,则会执行新旧 vnode 的对比,即著名的patch
算法,将修改的部分更新到页面。
1 | Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { |
上面的__patch__
就会执行我们的初始化或 diff 过程,后续会专门来说。
小结
粗略的总结一下 Vue 在初始化时都干了些什么:
- 合并构造器及构造器父级上定义的 options。
- 给 vm 添加$parent、$root、$children、$refs 等属性
- 给 vm 添加$slots、$scopeSlots、_c、$createElement、$attrs、\$listeners 等属性
- 调用
beforeCreate
钩子 - 解析 inject,将拿到的值放到 vm 上
- 处理 props、methods、data、computed、watch.,将他们统统挂载到 vm 上
- 解析 provide
- 调用
created
钩子 - 编译
template
,拿到render
函数 - 调用
beforeMount
钩子 - 调用
render
获取vnode
- 调用
patch
创建真实 dom 并渲染到页面 - 调用
mounted
钩子