• vue的初始化流程(vue2.x运行机制)Vue 2.x 的运行机制
  • vue组件的生命周期
  • 描述组件渲染和更新过程
  • vue模板编译原理

vue运行机制

初始化流程

  • 创建vue实例

  • initMixin(Vue) 初始化生命周期,初始化事件中心、初始化渲染,执行beforeCreate钩子函数,初始化props/methods/data/computed/watch,执行created函数

  • 初始化之后,调用vm.$mount(vm.$options.el)方法对实例进行挂载(挂载核心包括模板编译,渲染以及 更新三个过程)

  • 如果没有在Vue实例上定义render方法而是定义了template,那么需要经历编译阶段。需要先将template 字符串通过 compileToFunctions函数 编译成 render function,template 字符串编译步骤如下:

    • parse正则解析template字符串形成AST描述符(抽象语法树,是源代码的抽象语法结构的树状表现形式)
    • optimize标记静态节点(跳过diff算法, vue Diff算法是逐层进行比对,只有同层级的节点进行比对,因此时间的复杂度只有 O(n))
    • generate将AST转化成 render function 字符串
  • 编译成render function之后,调用vm.$mount(vm.$mount(vm.$options.el))中的mountComponent方法,然后调用callHook(vm, ‘beforeMount’)钩子函数,然后核心是实例化一个渲染Watcher,在它的回调函数(初始化的时候执行,以及组件实例中监测到数据发生变化时执行)中调用updateComponent方法_update(此方法调用_render方法生成虚拟Node,最终调用_update方法更新DOM)

    1
    2
    3
    4
    5
    callHook(vm, 'beforeMount')
    updateComponent = () => {
    vm._update(vm._render(), hydrating)
    }
    callHook(vm, 'mounted')
    • 调用_render方法将render function渲染成虚拟的Node
    • 生成虚拟DOM树后,需要将虚拟DOM树转化成真实的DOM节点,此时需要调用_update方法,update方法又会调用patch方法把虚拟DOM转换成真正的DOM节点

(编译成render function字符串之后的,生成虚拟dom, 然后是_update -> patch方法这块在虚拟dom重点介绍)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevVnode = vm._vnode;
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render 初始化渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};

初始化过程中,描述数据的响应式流程:

响应式流程

在init的时候会利用Object.defineProperty方法(不兼容IE8)监听Vue实例的响应式数据的变化从而实现数据劫持能力(利用了JavaScript对象的存取器属性get和set)。在初始化流程中的编译阶段,当render function被渲染的时候,会读取Vue实例中和视图相关的响应式数据(这个就是重点,视图和响应式数据是如何关联的),此时会触发getter函数进行依赖收集(将观察者Watcher对象存放到当前闭包的订阅者Dep的subs中),此时的数据劫持功能和观察者模式就实现了一个MVVM模式中的Binder,之后就是正常的渲染和更新流程。

当数据发生变化或者视图导致的数据发生了变化时,会触发数据劫持的setter函数,setter会通知初始化依赖收集中的Dep中的和视图相应的Watcher,告知需要重新渲染视图,Wather就会再次通过update方法来更新视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// new Vue 执行流程。
// 1. Vue.prototype._init(option) => initState => initData
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 render 方法。
// 4. Vue.prototype.$mount 调用上面的 render 方法挂载 dom。
// 5. mountComponent

// 6. 创建 Watcher 实例
const updateComponent = () => {
vm._update(vm._render());
};
// 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
new Watcher(vm, updateComponent);

// 7. new Watcher 会执行 Watcher.get 方法
// 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
// 9. updateComponent 会执行 vm._update(vm._render())

// 10. 调用 vm._render 生成虚拟 dom
Vue.prototype._render = function (): VNode {
const vm: Component = this;
const { render } = vm.$options;
let vnode = render.call(vm._renderProxy, vm.$createElement);
return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚拟 dom
Vue.prototype._update = function (vnode: VNode) {
const vm: Component = this;
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
// 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom。

概括起来:

  • 从 new Vue 开始,首先通过 get、set 监听 Data 中的数据变化,同时创建 Dep 用来搜集使用该 Data 的 Watcher。
  • 编译模板,创建 Watcher,并将 Dep.target 标识为当前 Watcher。
  • 编译模板时,如果使用到了 Data 中的数据,就会触发 Data 的 get 方法,然后调用 Dep.addSub 将 Watcher 搜集起来。
  • 数据更新时,会触发 Data 的 set 方法,然后调用 Dep.notify 通知所有使用到该 Data 的 Watcher 去更新 DOM。

生命周期的理解(精简,切中要害)

created/mounted/updated/destroyed,以及对应的before钩子。分别是创建=>挂载=>更新=>销毁
Vue源码中定义了一个mergeHook函数来遍历一个常量数组LIFECYCLE_HOOKS,该数组实际上是由与生命周期钩子同名的字符串组成的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];

其中,还有activated & deactivated(keep-alive 组件激活/停用)、errorCaptured(v2.5 以上版本有的一个钩子,用于处理错误)
serverPrefetch: 这是用来处理ssr的。允许我们在渲染过程中“等待”异步数据。可在任何组件中使用,而不仅仅是路由组件。

vue生命周期详细流程

初始化流程

new Vue
从 new Vue(options) 开始作为入口,Vue 只是一个简单的构造函数,内部是这样的:

1
2
3
function Vue (options) {
this._init(options)
}

进入了 _init 函数之后,先初始化了一些属性,然后开始第一个生命周期:

1
callHook(vm, 'beforeCreate')

beforeCreate

  • 初始化 inject
  • 初始化 state
    初始化 props
    初始化 methods
    初始化 data
    初始化 computed
    初始化 watch
  • 初始化 provide
    然后进入 created 阶段:
    1
    callHook(vm, 'created')

    created被调用完成

    调用 $mount 方法,开始挂载组件到 dom 上。

如果使用了 runtime-with-compile 版本,则会把你传入的 template 选项,或者 html 文本,通过一系列的编译生成 render 函数。

  • 编译这个 template,生成 ast 抽象语法树。
  • 优化这个 ast,标记静态节点。(渲染过程中不会变的那些节点,优化性能)。
  • 根据 ast,生成 render 函数。
    1
    2
    3
    4
    5
    const ast = parse(template.trim(), options)
    if (options.optimize !== false) {
    optimize(ast, options)
    }
    const code = generate(ast, options)
    如果果是脚手架cli搭建的项目的话,这一步 vue-cli 已经帮你做好了,所以就直接进入 mountComponent 函数。
    那么,确保有了 render 函数后,我们就可以往渲染的步骤继续进行了

beforeMount被调用完成

把 渲染组件的函数 定义好,具体代码是:

1
2
3
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

拆解来看,vm._render 其实就是调用我们上一步拿到的 render 函数生成一个 vnode,而 vm._update 方法则会对这个 vnode 进行 patch 操作,帮我们把 vnode 通过 createElm函数创建新节点并且渲染到 dom节点 中。

接下来就是执行这段代码了,是由 响应式原理 的一个核心类 Watcher 负责执行这个函数,为什么要它来代理执行呢?因为我们需要在这段过程中去 观察 这个函数读取了哪些响应式数据,将来这些响应式数据更新的时候,我们需要重新执行 updateComponent 函数。

如果是更新后调用 updateComponent 函数的话,updateComponent 内部的 patch 就不再是初始化时候的创建节点,而是对新旧 vnode 进行 diff,最小化的更新到 dom节点 上去。
如果遇到 子组件,那么就会优先开始子组件的构建过程,也就是说,从 beforeCreated 重新开始。这是一个递归的构建过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
也就是说,如果我们有 父 -> 子 -> 孙 这三个组件,那么它们的初始化生命周期顺序是这样的:
父 beforeCreate
父 create
父 beforeMount
子 beforeCreate
子 create
子 beforeMount
孙 beforeCreate
孙 create
孙 beforeMount
孙 mounted
子 mounted
父 mounted

mounted被调用完成

到此为止,组件的挂载就完成了,初始化的生命周期结束。

更新流程

当一个响应式属性被更新后,触发了 Watcher 的回调函数,也就是 vm._update(vm._render()),在更新之前,会先调用刚才在 before 属性上定义的函数,也就是

1
callHook(vm, 'beforeUpdate')

注意,由于 Vue 的异步更新机制,beforeUpdate 的调用已经是在 nextTick 中了。

1
2
3
4
5
6
7
8
9
10
11
nextTick(flushSchedulerQueue)

function flushSchedulerQueue {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// callHook(vm, 'beforeUpdate')
watcher.before()
}
}
}

beforeUpdate被调用完成

然后经历了一系列的 patch、diff 流程后,组件重新渲染完毕,调用 updated 钩子。

注意,这里是对 watcher 倒序 updated 调用的。

也就是说,假如同一个属性通过 props 分别流向 父 -> 子 -> 孙 这个路径,那么收集到依赖的先后也是这个顺序,但是触发 updated 钩子确是 孙 -> 子 -> 父 这个顺序去触发的。

1
2
3
4
5
6
7
8
9
10
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}

updated被调用完成

至此,渲染更新流程完毕。

销毁流程

在刚刚所说的更新后的 patch 过程中,如果发现有组件在下一轮渲染中消失了,比如 v-for 对应的数组中少了一个数据。那么就会调用 removeVnodes 进入组件的销毁流程。

removeVnodes 会调用 vnode 的 destroy 生命周期,而 destroy 内部则会调用我们相对比较熟悉的 vm.$destroy()。(keep-alive 包裹的子组件除外)

这时,就会调用 callHook(vm, ‘beforeDestroy’)

beforeDestroy被调用完成

之后就会经历一系列的清理逻辑,清除父子关系、watcher 关闭等逻辑。但是注意,$destroy 并不会把组件从视图上移除,如果想要手动销毁一个组件,则需要我们自己去完成这个逻辑。

然后,调用最后的 callHook(vm, ‘destroyed’)

destroyed被调用完成

组件渲染和更新过程

组件渲染过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

// 从模板编译开始,当发现一个自定义组件时,会执行以下函数
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement

// 6. createComponent
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// $options._base 其实就是全局 Vue 构造函数,在初始化时 initGlobalAPI 中定义的:Vue.options._base = Vue
const baseCtor = context.$options._base;
// Ctor 就是 Vue 组件中 <script> 标签下 export 出的对象
if (isObject(Ctor)) {
// 将组件中 export 出的对象,继承自 Vue,得到一个构造函数
// 相当于 Vue.extend(YourComponent)
Ctor = baseCtor.extend(Ctor);
}
const vnode = new VNode(`vue-component-${Ctor.cid}`);
return vnode;
}

// 7. 实现组件继承 Vue,并调用 Vue._init 方法,进行初始化
Vue.extend = function (extendOptions: Object): Function {
const Super = this;
const Sub = function VueComponent(options) {
// 调用 Vue.prototype._init,之后的流程就和首次加载保持一致
this._init(options);
};
// 原型继承,相当于:Component extends Vue
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
return Sub;
};

参考