生命周期之间所做事情
本文不涉及 keep-alive 的场景和错误处理的场景
初始化流程
new Vue
从 new Vue(options) 开始作为入口,Vue 只是一个简单的构造函数,内部为
function Vue(options) {
this._init(options);
}
进入_init 函数后,先初始化了一些属性,然后开始第一个生命周期
callHook(vm, 'beforeCreate');
beforeCreate 被调用完成
beforeCreate 之后
- 初始化 inject
- 初始化 state
- 初始化 props
- 初始化 methods
- 初始化 data
- 初始化 computed
- 初始化 initWatch
- 初始化 provide
然后进入 created 阶段
callHook(vm, 'create');
create 被调用完成
调用 $mount 方法,开始挂载组件到 dom 上
如果使用了 rentime-with-compile 版本,则会把你传入的 template 选项,或者 html 文本,通过一系列的编译生成 render 函数
- 编译这个 template,生成 ast 抽象语法树
- 优化这个 ast,标记静态节点,即渲染过程中不会变的那些节点,优化性能
- 根据 ast,生成 render 函数
具体对应的代码为
const ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
const code = generate(ast, options);
如果是脚手架搭建的项目的话,这一步已经由 vue-cli 做好了,所以就直接进入 mountComponent 函数
确保有了 render 函数后,就可以往渲染的步骤继续进行了
beforeMount 被调用完成
把渲染组件的函数定义好,具体代码为
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
vm._render其实就是调用上一步拿到的 render 函数生成一个 vnodevm._update方法会对这个 vnode 进行 patch 操作,帮助我们把 vnode 通过 createElm 函数创建新节点并且渲染到 dom 节点中
接下来就是执行这段代码,由响应式原理的一个核心类 Watcher 负责执行这个函数,因为我们需要在这段过程中去观察这个函数读取了哪些响应数据,将来这些响应式数据更新的时候,需要重新执行 updateComponent 函数
如果是更新后调用 updateComponent 函数,内部的 patch 不再是初始化时候的创建节点,而是对新旧 vnode 进行 diff,找出最小化的更新
Watcher 代码为
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
},
},
true /* isRenderWatcher */
);
此处在 before 属性上定义了 beforeUpdate 函数,也就是说在 Watcher 被响应式属性的更新触发之后,重新渲染新视图之前,会先调用 beforeUpdate 声明周期
如果在渲染过程中,遇到了子组件,则会调用 createComponent 函数,其内部会做一件事
Ctor = baseCtor.extend(Ctor);
在普通的场景下,这就是 Vue.extend 生成的构造函数,可以理解为继承自 Vue 函数
注:vnode 也有自己的生命周期,只是平时开发接触不到
子组件会有自己的 init 周期,这个周期内部会做这样的事情
const child = createComponentInstanceForVnode(vnode);
child.$mount(vnode.elm);
createComponentInstanceForVnode 内部会去调用子组件的构造函数
new vnode.componentOptions.Ctor(options);
这个构造函数内部是这样的
const Sub = function VueComponent(options) {
this._init(options);
};
这个 _init 就是文章开头的那个函数,其实就是:遇到子组件,就会优先开始子组件的构建过程,从 beforeCreate 重新开始
组件之间的初始化生命周期
父 beforeCreate
父 create
父 beforeMount
子 beforeCreate
子 create
子 beforeMount
孙 beforeCreate
孙 create
孙 beforeMount
孙 mounted
子 mounted
父 mounted
mounted 被调用完成
组件挂载完成,初始化的生命周期结束
更新流程
当一个响应式属性被更新后,会触发 Watcher 的回调函数,也就是 vm._update(vm._render()),在更新之前会先调用 before 属性上定义的函数
callHook(vm, 'beforeUpdate');
由于 Vue 的异步更新机制,beforeUpdate 的调用已经是在 nextTick 中了,具体代码为
nextTick(flushSchedulerQueue);
function flushSchedulerQueue() {
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before(); // 即 callHook(vm, 'beforeUpdate')
}
}
}
beforeUpdate 被调用完成
经历了一系列的 patch、diff 之后,组件重新渲染完毕,调用 update 钩子
这里对 updated 调用是倒序的,如:props 流向为父--子--孙,触发 updated 钩子为孙--子--父。具体代码为
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 过程中,如果发现有组件在下一轮渲染中消失,则会调用 removeVnodes 进入组件销毁流程
removeVnodes 会调用 vnode 的 destroy 生命周期,而 destroy 内部则会调用 vm.$destroy()(keep-alive 包裹的子组件除外)
这时会调用 callHook(vm, 'beforeDestroy')
beforeDestroy 被调用完成
之后会经历一系列的清理逻辑:清除父子关系、watcher 关闭等
但是 $destroy 并不会把组件从视图上移除,如果想要手动销毁一个组件,则需要自己去完成这个逻辑(根据官方文档解释,$destroy 只是清理它和其他实例的连接,以及解除指令和事件监听器,断掉虚拟 dom 和真实 dom 之间的联系,并没有真正回收这个实例,若此时浏览器已经渲染了 dom,则该 dom 可能不会被移除)
至于 vue 实例什么时候回收,其本质上是一个 js 的内存回收问题
最后调用 callHook(vm, 'destroyed')
destroyed 被调用完成
组件销毁完成