生命周期之间所做事情

本文不涉及 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 函数生成一个 vnode
  • vm._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 被调用完成

组件销毁完成

Last Updated:
Contributors: af