Vue.js 生命周期深度解析:从实例创建到销毁
核心问题
new Vue()之后发生了什么?(创建过程)- 数据改变之后,又发生了什么?(更新过程)
一、 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)和事件系统都尚未初始化。你无法访问
data、props、computed、methods等。
3. 注入流程 (Injections & Reactivity)
这是 beforeCreate 和 created 之间发生的核心步骤。
处理配置项:初始化
props,methods,data,computed,provide,inject等。数据响应式:
- 调用
observe(data)函数,递归地将data对象中的所有属性转换为响应式数据(通过Object.defineProperty的get和set)。 - 此时,数据已经具备了响应能力,但还未挂载到实例上。
- 调用
代理模式:
- 将
data、props等配置中的属性代理到 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.data和this.xxx方法。 - 常用于进行异步数据请求。
- 注意:此时虚拟 DOM(VNode)和真实 DOM 都还未生成,所以无法访问 DOM 元素。
5. 编译与挂载 (beforeMount 与 mounted 之间)
编译模板:
- 检查实例是否有
render函数。 - 如果没有,并且有
template选项,Vue 会在运行时将template字符串编译成render函数。 - 对于
.vue单文件组件,这一步在构建时由vue-loader完成,性能更高。
- 检查实例是否有
运行
beforeMount钩子:render函数已经准备好,即将开始渲染。- 真实 DOM 尚未创建。
核心渲染流程 (
updateComponent):- Vue 会创建一个
Watcher(渲染 Watcher),并传入一个核心函数updateComponent。 updateComponent的工作是:- 执行
this._render():调用render函数,生成组件的虚拟 DOM 树(VNode)。 - 执行
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 });- Vue 会创建一个
6. 运行 mounted 钩子
- 所有组件(包括所有子组件)的 DOM 都已成功渲染并挂载到页面上。
- 此时可以安全地进行 DOM 操作,例如通过
$refs访问子组件实例或 DOM 元素。
二、数据改变后发生了什么?(更新过程)
更新流程概览
Parent: beforeUpdateChild: beforeUpdateGrandchild: beforeUpdateGrandchild: updatedChild: updatedParent: updated
更新过程同样是深度优先的。父组件的更新会触发子组件的更新(如果传递的 props 改变了),父组件会等待所有受影响的子组件都更新完毕后,才调用自己的 updated 钩子。
详细步骤分解
触发更新:
- 当一个响应式数据(如
data或props)被修改时,它的set拦截器会被触发。 set拦截器会通知所有依赖该数据的Watcher。
- 当一个响应式数据(如
调度 Watcher:
- 被通知的
Watcher不会立即执行,而是被添加到一个微任务队列 (nextTick) 中。 - 这种异步批量更新机制可以有效防止因连续多次修改数据而导致的重复渲染,提升性能。
- 被通知的
运行
beforeUpdate钩子:- 在组件即将重新渲染之前调用。
- 此时数据已经是最新的,但 DOM 仍然是旧的。
重新渲染:
Watcher被执行,再次调用updateComponent函数。this._render()重新执行,根据最新的数据生成一棵新的 VNode 树,并重新收集依赖。this._update(newVNode)被调用,它内部会执行patch(oldVNode, newVNode)。
patch过程(Diff 算法):patch函数对比新旧两棵 VNode 树的差异。- 它会尽可能地复用已有的 DOM 元素,只对发生变化的部分进行创建、更新、移动或删除操作,实现最小化 DOM 更新。
- 组件节点对比:如果新旧 VNode 都是同一个组件(类型和 key 相同),则该组件实例会被复用。新的
props会被传递给该子组件实例,这个过程会触发子组件自身的更新流程(从beforeUpdate开始)。
运行
updated钩子:- 当组件和其所有子组件的 DOM 都已更新完毕后调用。
- 此时可以访问更新后的 DOM。
- 注意:避免在此钩子中修改数据,否则可能导致无限循环更新。
三、组件销毁过程
销毁流程概览
Parent: beforeDestroyChild: beforeDestroyGrandchild: beforeDestroyGrandchild: destroyedChild: destroyedParent: destroyed
销毁过程也是深度优先的,父组件会先确保其所有子组件都被销毁,然后才销毁自己。
详细步骤分解
触发销毁:
- 通常由
v-if指令从true变为false触发。 - 或父组件被销毁,导致其所有子组件也被销毁。
- 通常由
运行
beforeDestroy钩子:- 在实例被销毁前调用。
- 此时实例上的所有东西(
data,methods,watchers等)仍然可用。 - 这是清理工作的最佳时机,例如清除定时器 (
clearInterval)、解绑自定义的全局事件监听器等。
递归销毁:
- 组件会递归地调用其所有子组件的销毁方法。
- 移除实例上的所有
Watcher和事件监听器。
运行
destroyed钩子:- 实例和其所有相关内容(指令绑定、DOM 节点等)都已被完全移除。
- 组件的生命周期至此结束。