Skip to content

Vue.js 生命周期深度解析:从实例创建到销毁

核心问题

  1. new Vue() 之后发生了什么?(创建过程
  2. 数据改变之后,又发生了什么?(更新过程

一、 new Vue() 之后发生了什么?(创建/挂载过程)

这个过程不仅适用于 new Vue() 创建的根实例,也适用于每个子组件的实例化。

挂载流程概览

Parent: beforeCreate -> Parent: created -> Parent: beforeMountChild: beforeCreate -> Child: created -> Child: beforeMountGrandchild: beforeCreate -> Grandchild: created -> Grandchild: beforeMountGrandchild: mountedChild: mountedParent: mounted

这是一个深度优先的递归过程:父组件的挂载流程会暂停,等待所有子组件都挂载完毕后,才会继续执行。

详细步骤分解

1. 初始化 (beforeCreate 之前)

  • 创建一个 Vue 实例。
  • 初始化实例的私有属性,如以 _$ 开头的属性。

2. 运行 beforeCreate 钩子

  • 这是生命周期中第一个被调用的钩子。
  • 此时,实例刚刚被创建,数据观测(Reactivity)和事件系统都尚未初始化。你无法访问 datapropscomputedmethods 等。

3. 注入流程 (Injections & Reactivity)

这是 beforeCreatecreated 之间发生的核心步骤。

  • 处理配置项:初始化 props, methods, data, computed, provide, inject 等。

  • 数据响应式

    • 调用 observe(data) 函数,递归地将 data 对象中的所有属性转换为响应式数据(通过 Object.definePropertygetset)。
    • 此时,数据已经具备了响应能力,但还未挂载到实例上。
  • 代理模式

    • dataprops 等配置中的属性代理到 Vue 实例 this 上。这就是为什么我们可以通过 this.someData 来访问 data 对象中的 someData 属性。
    javascript
    // 伪代码:代理 data 属性到 this
    const data = options.data(); // 获取 data 对象
    observe(data); // 将 data 对象变为响应式
    
    // 遍历 data 对象的 key
    for (const key in data) {
      // 将访问 this.key 代理到访问 data.key
      Object.defineProperty(this, key, {
        get() {
          return data[key];
        },
        set(newValue) {
          data[key] = newValue;
        }
      });
    }
  • 方法绑定

    • 遍历 methods 配置,并将每个方法通过 .bind(this) 绑定到 Vue 实例上。这确保了在方法内部 this 始终指向正确的 Vue 实例。
    javascript
    // 伪代码:绑定 methods
    for (const methodName in options.methods) {
      this[methodName] = options.methods[methodName].bind(this);
    }

4. 运行 created 钩子

  • 此时,数据响应式、计算属性、方法等都已准备就绪
  • 可以访问 this.datathis.xxx 方法。
  • 常用于进行异步数据请求。
  • 注意:此时虚拟 DOM(VNode)和真实 DOM 都还未生成,所以无法访问 DOM 元素。

5. 编译与挂载 (beforeMountmounted 之间)

  • 编译模板

    • 检查实例是否有 render 函数。
    • 如果没有,并且有 template 选项,Vue 会在运行时template 字符串编译成 render 函数。
    • 对于 .vue 单文件组件,这一步在构建时由 vue-loader 完成,性能更高。
  • 运行 beforeMount 钩子

    • render 函数已经准备好,即将开始渲染。
    • 真实 DOM 尚未创建。
  • 核心渲染流程 (updateComponent):

    • Vue 会创建一个 Watcher (渲染 Watcher),并传入一个核心函数 updateComponent
    • updateComponent 的工作是:
      1. 执行 this._render():调用 render 函数,生成组件的虚拟 DOM 树(VNode)。
      2. 执行 this._update(vnode):接收生成的 VNode,并调用 patch 函数将其转换为真实 DOM。
    • 依赖收集:在 this._render() 执行期间,所有被访问到的响应式数据都会将当前的 Watcher 记录为依赖。
    • patch 过程(首次渲染)
      • 因为是首次渲染,不存在旧的 VNode 树。
      • patch 函数会遍历新的 VNode 树,为每个 VNode 创建对应的真实 DOM 元素,并将其挂载到页面上。
      • 遇到子组件:如果 patch 过程中遇到一个组件类型的 VNode,它会递归地进入该子组件的实例化流程(从上面的步骤 1 开始),直到最深层的子组件被挂载。
    javascript
    // 伪代码:渲染 Watcher 的核心逻辑
    new Watcher(this, () => {
      // 这个函数就是 updateComponent
      const vnode = this._render(); // 1. 生成 VNode,并收集依赖
      this._update(vnode);       // 2. 将 VNode patch 到真实 DOM
    });

6. 运行 mounted 钩子

  • 所有组件(包括所有子组件)的 DOM 都已成功渲染并挂载到页面上。
  • 此时可以安全地进行 DOM 操作,例如通过 $refs 访问子组件实例或 DOM 元素。

二、数据改变后发生了什么?(更新过程)

更新流程概览

Parent: beforeUpdateChild: beforeUpdateGrandchild: beforeUpdateGrandchild: updatedChild: updatedParent: updated

更新过程同样是深度优先的。父组件的更新会触发子组件的更新(如果传递的 props 改变了),父组件会等待所有受影响的子组件都更新完毕后,才调用自己的 updated 钩子。

详细步骤分解

  1. 触发更新

    • 当一个响应式数据(如 dataprops)被修改时,它的 set 拦截器会被触发。
    • set 拦截器会通知所有依赖该数据的 Watcher
  2. 调度 Watcher

    • 被通知的 Watcher 不会立即执行,而是被添加到一个微任务队列 (nextTick) 中。
    • 这种异步批量更新机制可以有效防止因连续多次修改数据而导致的重复渲染,提升性能。
  3. 运行 beforeUpdate 钩子

    • 在组件即将重新渲染之前调用。
    • 此时数据已经是最新的,但 DOM 仍然是旧的。
  4. 重新渲染

    • Watcher 被执行,再次调用 updateComponent 函数。
    • this._render() 重新执行,根据最新的数据生成一棵新的 VNode 树,并重新收集依赖。
    • this._update(newVNode) 被调用,它内部会执行 patch(oldVNode, newVNode)
  5. patch 过程(Diff 算法)

    • patch 函数对比新旧两棵 VNode 树的差异。
    • 它会尽可能地复用已有的 DOM 元素,只对发生变化的部分进行创建、更新、移动或删除操作,实现最小化 DOM 更新。
    • 组件节点对比:如果新旧 VNode 都是同一个组件(类型和 key 相同),则该组件实例会被复用。新的 props 会被传递给该子组件实例,这个过程会触发子组件自身的更新流程(从 beforeUpdate 开始)。
  6. 运行 updated 钩子

    • 当组件和其所有子组件的 DOM 都已更新完毕后调用。
    • 此时可以访问更新后的 DOM。
    • 注意:避免在此钩子中修改数据,否则可能导致无限循环更新。

三、组件销毁过程

销毁流程概览

Parent: beforeDestroyChild: beforeDestroyGrandchild: beforeDestroyGrandchild: destroyedChild: destroyedParent: destroyed

销毁过程也是深度优先的,父组件会先确保其所有子组件都被销毁,然后才销毁自己。

详细步骤分解

  1. 触发销毁

    • 通常由 v-if 指令从 true 变为 false 触发。
    • 或父组件被销毁,导致其所有子组件也被销毁。
  2. 运行 beforeDestroy 钩子

    • 在实例被销毁前调用。
    • 此时实例上的所有东西(data, methods, watchers等)仍然可用。
    • 这是清理工作的最佳时机,例如清除定时器 (clearInterval)、解绑自定义的全局事件监听器等。
  3. 递归销毁

    • 组件会递归地调用其所有子组件的销毁方法。
    • 移除实例上的所有 Watcher 和事件监听器。
  4. 运行 destroyed 钩子

    • 实例和其所有相关内容(指令绑定、DOM 节点等)都已被完全移除。
    • 组件的生命周期至此结束。