Vue 2 的生命周期钩子一直是前端面试的“高频考点”,但在回答时,你是不是还在死背那八个生命周期钩子?
一百个人里面至少得有九十个人都这么回答,面试官早都听腻了!
你是否想过,面试官真正想问的到底是什么?他们考察的仅仅是想看你知不知道这 8 个生命周期钩子?
假如面试官继续追问你稍微再深入一点的问题,你是否回答的上来呢?比如:
在一个复杂的组件树中(例如父组件有多个子组件),生命周期钩子的执行顺序是怎样的?
当使用
keep-alive包裹组件时,生命周期会发生什么变化?当组件缓存起来之后,activated和deactivated钩子分别在什么场景下执行?当组件从
active状态切换为inactive时,deactivated钩子执行了,但为什么不会调用destroyed?”
本文将从最基础的生命周期钩子开始讲起,一步步剖析 Vue 2 中组件在不同场景下的生命周期表现。无论你是为面试做准备,还是想要更深入地理解 Vue 2 的生命周期原理,这篇文章都将带你彻底搞懂它!
Vue 2 中的生命周期钩子函数
在 Vue 2 中,生命周期钩子函数是 Vue 实例在其生命周期的不同阶段自动调用的特殊方法。这些钩子函数让你能够在不同的阶段执行代码,如初始化、更新和销毁等。
Vue 实例的生命周期可以分为 创建阶段、挂载阶段、更新阶段 和 销毁阶段。
1. 创建阶段 (Before Create → Created)
beforeCreate :在实例初始化之后、数据观测和事件配置之前被调用。此时,数据和事件都还不可用。
beforeCreate() {
console.log('beforeCreate');
}
created :实例已经创建完成,数据观测、事件和侦听器都已设置完成,但 DOM 还没有挂载到页面上。
created() {
console.log('created');
}
2. 挂载阶段 (Before Mount → Mounted)
beforeMount :在挂载开始之前被调用,即模板首次渲染之前。
beforeMount() {
console.log('beforeMount');
}
mounted :Vue 实例挂载到 DOM 上之后立即调用。这时你可以进行 DOM 操作,数据已经绑定到 DOM 元素。
mounted() {
console.log('mounted');
}
3. 更新阶段 (Before Update → Updated)
beforeUpdate :数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
beforeUpdate() {
console.log('beforeUpdate');
}
updated :数据更新且虚拟 DOM 重新渲染和打补丁之后调用。
updated() {
console.log('updated');
}
4. 销毁阶段 (Before Destroy → Destroyed)
beforeDestroy :在实例销毁之前调用。此时,实例仍然完全可用,但你可以进行清理工作,比如清除定时器、移除事件监听器等。
beforeDestroy() {
console.log('beforeDestroy');
}
destroyed :Vue 实例销毁后调用。此时,实例已完全销毁,所有的事件监听器和子实例都已被清除。
destroyed() {
console.log('destroyed');
}
生命周期钩子函数小结
| 生命周期阶段 | 钩子函数 | 说明 |
|---|---|---|
| 创建阶段 | beforeCreate | 组件实例刚创建,data 和 el 尚未初始化 |
created | 数据已初始化,但组件尚未挂载到 DOM | |
| 挂载阶段 | beforeMount | 组件即将挂载到 DOM,模板已编译但尚未应用到页面 |
mounted | 组件已挂载到 DOM,DOM 元素可访问 | |
| 更新阶段 | beforeUpdate | 响应式数据发生变化,DOM 尚未更新 |
updated | 数据变化且 DOM 更新后 | |
| 销毁阶段 | beforeDestroy | 组件销毁前,仍可访问实例 |
destroyed | 组件已销毁,所有事件和数据绑定均已清除 |
我们可以使用一个示例来清晰地观察各个生命周期的工作过程。该示例中,有两个按钮:“更新数据”按钮用于触发更新生命周期钩子,“销毁组件”按钮用于触发销毁生命周期钩子。
<template>
<div id="app">
<button @click="change">更新数据</button>
<button @click="destroyComponent">销毁组件</button>
<p>{{ msg }}</p>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
msg: "Hello World",
};
},
// 1. 创建阶段
beforeCreate() {
console.log(
"beforeCreate: 组件实例刚创建,data和el尚未初始化,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
created() {
console.log(
"created: 数据已初始化,但组件尚未挂载到DOM,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
// 2. 挂载阶段
beforeMount() {
console.log(
"beforeMount: 组件即将挂载到DOM,模板已渲染但还未应用到页面,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
mounted() {
console.log(
"mounted: 组件已挂载到DOM,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
// 3. 更新阶段
beforeUpdate() {
console.log(
"beforeUpdate: 数据发生变化,DOM尚未更新,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
updated() {
console.log(
"updated: 数据变化且DOM已更新,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
// 4. 销毁阶段
beforeDestroy() {
console.log(
"beforeDestroy: 组件即将被销毁,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
destroyed() {
console.log(
"destroyed: 组件已被销毁,所有事件和绑定都已清除,此时data中的数据msg的值为:",
this.msg,
",el的值为:",
this.$el
);
},
methods: {
change() {
// 更新数据
this.msg = "msg 被我修改啦!";
},
destroyComponent() {
// 销毁组件
this.$destroy();
},
},
};
</script>
<style></style>
我们打开控制台可以看到:
组件生命周期钩子函数的执行顺序为:
创建阶段:beforeCreate -> created
挂载阶段:beforeMount -> mounted
此时当我们点击“更新数据”按钮时,组件生命周期钩子函数进入 更新阶段(当数据变化时触发):beforeUpdate -> updated,如下图:
当我们点击“销毁组件”按钮时,组件生命周期钩子函数进入 销毁阶段:beforeDestroy -> destroyed,如下图:
控制台输出解析
创建阶段:组件初始化时会依次触发 beforeCreate 和 created 钩子函数。在 beforeCreate 阶段,组件实例的 data 和 el 尚未初始化,因此 this.msg 和 this.$el 尚不可用。而在 created 阶段,组件实例的数据已经初始化完成,可以访问 msg 的值。
挂载阶段:beforeMount 和 mounted 分别在组件挂载到 DOM 之前和之后触发。在 beforeMount中,模板已经编译,但尚未挂载到页面。此时 this.$el 仍然是未挂载的 DOM 实例。mounted 钩子则在组件挂载后触发,此时可以正常访问 DOM 元素和初始化的数据。
更新阶段:当我们点击“更新数据”按钮时,触发 change 方法修改了 msg 的值。修改数据后,组件的 beforeUpdate 钩子会先执行,表示数据发生了变化但 DOM 尚未更新。接着在 updated 钩子中,DOM 已同步更新完毕,可以看到最新的渲染结果。
销毁阶段:当点击“销毁组件”按钮时,会执行 destroyComponent 方法。此时,组件进入销毁阶段,依次执行 beforeDestroy 和 destroyed 钩子。beforeDestroy 表示组件即将被销毁,我们依然可以访问组件的数据和 DOM。destroyed 则是组件完全销毁后的钩子,此时所有事件和绑定均已清除,组件已从页面上彻底移除。
需要注意的是,尽管组件已被销毁,但是
data中的数据并不会被自动清空。这是因为 Vue 的数据绑定是基于 JavaScript 的对象引用。当组件被销毁时,data对象仍然存在于内存中,因为它可能被其他对象或作用域引用。这种设计允许我们在beforeDestroy和destroyed钩子中继续访问这些数据,以便进行必要的清理和处理。例如,我们可以在组件销毁前将数据保存到 Vuex 等全局状态管理器中,或者在其他地方进行进一步的处理。因此,虽然组件实例已被清除,但数据本身并不会自动销毁,直到没有任何引用指向它们。
引入子组件时父子组件的生命周期执行顺序
在上文中我们知道了 Vue 2 中的各个生命周期钩子函数的触发顺序。然而,当组件间存在父子关系时,父子组件的生命周期钩子执行顺序会略有不同。
1.创建与挂载阶段
Vue 2 会在特定的生命周期阶段切换父子组件的钩子执行顺序,以确保父组件先初始化和挂载,而子组件在父组件准备好之后才进行相应的操作。
我们可以通过一个简单示例来观察这种顺序,新建一个子组件 MyList:
<template>
<div class="my-list">这里是子组件 List 的内容</div>
</template>
<script>
export default {
name: "MyList",
beforeCreate() {
console.log("子组件 MyList:beforeCreate");
},
created() {
console.log("子组件 MyList:created");
},
beforeMount() {
console.log("子组件 MyList:beforeMount");
},
mounted() {
console.log("子组件 MyList:mounted");
},
beforeUpdate() {
console.log("子组件 MyList:beforeUpdate");
},
updated() {
console.log("子组件 MyList:updated");
},
beforeDestroy() {
console.log("子组件 MyList:beforeDestroy");
},
destroyed() {
console.log("子组件 MyList:destroyed");
},
};
</script>
<style scoped></style>
然后我们在父组件引入它:
<template>
<div id="app">
<button @click="change">更新数据</button>
<button @click="destroyComponent">销毁组件</button>
<p>{{ msg }}</p>
<hr />
<my-list />
</div>
</template>
<script>
import MyList from "@/components/MyList.vue";
export default {
name: "App",
components: { MyList },
data() {
return {
msg: "Hello World",
};
},
// ...这里我们省略第一节示例中的生命周期部分代码
methods: {
change() {
// 更新数据
this.msg = "msg 被我修改啦!";
},
destroyComponent() {
// 销毁组件
this.$destroy();
},
},
};
</script>
<style></style>
在控制台中可以观察到父子组件生命周期钩子的执行顺序:
由上图,父子组件执行顺序为:
父组件:beforeCreate -> created -> beforeMount
子组件:beforeCreate -> created -> beforeMount -> mounted
父组件:mounted
即在挂载阶段,父组件的 beforeMount 钩子执行完毕后,才开始对子组件进行创建和挂载,待子组件的 mounted 执行完成,父组件才最终进入 mounted 钩子。这种顺序确保子组件在父组件完全挂载前已准备好。
注意,如果在子组件的
beforeCreate去尝试输出父组件传过来的msg时,会直接报错。因为在子组件的beforeCreate钩子中,子组件的props数据还没有完成初始化,因此无法获取父组件传递的数据。只有进入created钩子后,props数据才会被初始化并注入,才能正常访问和使用。
2.更新与销毁阶段
接下来我们再来看一下更新与销毁时的情况,我们可以在子组件 MyList.vue 中,接收来自父组件的 msg 数据:
<template>
<div class="my-list">
这里是子组件 MyList 的内容,父组件传来的 msg: {{ msg }}
</div>
</template>
<script>
export default {
name: "MyList",
props: {
msg: {
type: String,
required: true,
},
},
beforeCreate() {
console.log("子组件 MyList:beforeCreate");
},
created() {
console.log("子组件 MyList:created");
},
beforeMount() {
console.log("子组件 MyList:beforeMount");
},
mounted() {
console.log("子组件 MyList:mounted");
},
beforeUpdate() {
console.log("子组件 MyList:beforeUpdate");
},
updated() {
console.log("子组件 MyList:updated");
},
beforeDestroy() {
console.log("子组件 MyList:beforeDestroy");
},
destroyed() {
console.log("子组件 MyList:destroyed");
},
};
</script>
<style scoped></style>
在父组件增加传入 msg 数据:
<template>
<div id="app">
<button @click="change">更新数据</button>
<button @click="destroyComponent">销毁组件</button>
<p>{{ msg }}</p>
<hr />
<my-list :msg="msg" />
</div>
</template>
<script>
import MyList from "@/components/MyList.vue";
export default {
name: "App",
components: { MyList },
data() {
return {
msg: "Hello World",
};
},
// ...这里我们省略第一节示例中的生命周期部分代码
methods: {
change() {
// 更新数据
this.msg = "msg 被我修改啦!";
},
destroyComponent() {
// 销毁组件
this.$destroy();
},
},
};
</script>
<style></style>
此时我们可以点击更新数据按钮,得到如下结果:
我们可以发现父子组件执行顺序为:
父组件:beforeUpdate
子组件:beforeUpdate
子组件:updated
父组件:updated
在更新阶段,Vue 先触发父组件的
beforeUpdate钩子,表示父组件的数据变化即将影响组件树。紧接着,子组件会进入自己的
beforeUpdate钩子,然后完成更新,执行updated钩子。最后,父组件的
updated钩子会被触发。这样可以确保子组件的变化在父组件的更新前已经完成,保持了父子组件状态的一致性。
当我们点击销毁按钮时,得到如下结果:
我们可以发现父子组件执行顺序为:
父组件:beforeDestroy
子组件:beforeDestroy -> destroyed
父组件:destroyed
在销毁阶段,Vue 会先触发父组件的
beforeDestroy钩子,然后销毁子组件,确保其完全移除并清理后,才进入父组件的destroyed钩子销毁父组件。这一顺序确保了父组件在销毁之前,其下的所有子组件都已经处理完毕,保持了组件关系的完整性。
引入多个子组件时生命周期执行顺序
我们前面讲了关于引入子组件时父子组件的生命周期执行顺序,那么假如我们引入了多个子组件呢?
1.创建与挂载阶段
上面我们引入了一个 MyList 组件,假如我这里再新建一个 MyTable 组件:
<template>
<div class="my-table">这里是子组件 MyTable 的内容,父组件传来的 msg: {{ msg }}</div>
</template>
<script>
export default {
name: "MyTable",
name: "MyList",
props: {
msg: {
type: String,
required: true,
},
},
beforeCreate() {
console.log("子组件 MyTable:beforeCreate");
},
created() {
console.log("子组件 MyTable:created");
},
beforeMount() {
console.log("子组件 MyTable:beforeMount");
},
mounted() {
console.log("子组件 MyTable:mounted");
},
beforeUpdate() {
console.log("子组件 MyTable:beforeUpdate");
},
updated() {
console.log("子组件 MyTable:updated");
},
beforeDestroy() {
console.log("子组件 MyTable:beforeDestroy");
},
destroyed() {
console.log("子组件 MyTable:destroyed");
},
};
</script>
<style scoped></style>
然后我们在父组件进行引入并使用:
<template>
<div id="app">
<button @click="change">更新数据</button>
<button @click="destroyComponent">销毁组件</button>
<p>{{ msg }}</p>
<hr />
<my-list :msg="msg" />
<my-table :msg="msg" />
</div>
</template>
<script>
import MyList from "@/components/MyList.vue";
import MyTable from "@/components/MyTable.vue";
export default {
name: "App",
components: { MyTable, MyList },
data() {
return {
msg: "Hello World",
};
},
// ...这里我们省略第一节示例中的生命周期部分代码
methods: {
change() {
// 更新数据
this.msg = "msg 被我修改啦!";
},
destroyComponent() {
// 销毁组件
this.$destroy();
},
},
};
</script>
<style></style>
此时我们观察控制台:
通过观察控制台日志,我们可以发现多个子组件在父组件的生命周期执行过程中遵循以下顺序:
父组件:beforeCreate -> created -> beforeMount
子组件 MyList:beforeCreate -> created -> beforeMount
子组件 MyTable:beforeCreate -> created -> beforeMount
子组件 MyList:mounted
子组件 MyTable:mounted
父组件:mounted
在创建和挂载阶段,父组件的
beforeMount钩子执行完毕后,按照 模板顺序 依次开始对子组件进行创建和挂载,等待所有子组件的beforeMount钩子执行完毕后,再依次进入mounted钩子,等待所有子组件的mounted执行完成,父组件才最终进入mounted钩子。
2.更新与销毁阶段
此时我们可以点击更新数据按钮,得到如下结果:
通过观察控制台日志,我们可以发现多个子组件在父组件的生命周期执行过程中遵循以下顺序:
父组件:beforeUpdate
子组件 MyList:beforeUpdate
子组件 MyTable:beforeUpdate
子组件 MyList:updated
子组件 MyTable:updated
父组件:updated
在更新阶段,Vue 先触发父组件的
beforeUpdate钩子,表示父组件的数据变化即将影响组件树。紧接着,子组件按照 模板顺序 依次进入beforeUpdate钩子,然后完成更新,依次执行updated钩子。最后,父组件的updated钩子被触发。
当我们点击销毁按钮时,得到如下结果:
我们可以发现父子组件执行顺序为:
父组件:beforeDestroy
子组件 MyList:beforeDestroy -> destroyed
子组件 MyTable:beforeDestroy -> destroyed
父组件:destroyed
这种销毁顺序表明,父组件的
beforeDestroy先于所有子组件的销毁钩子执行。之后,子组件将按 模板顺序 依次执行beforeDestroy和destroyed,最后由父组件完成最终的destroyed钩子。
多个子组件的生命周期执行顺序小结
综上,我们可以发现在 Vue 中,当父组件包含多个子组件时,生命周期钩子的执行顺序遵循以下规则:
创建与挂载阶段:
- 父组件的
beforeMount钩子执行完毕后,子组件按模板顺序依次进入beforeCreate->created->beforeMount。 - 子组件的
beforeMount钩子全部执行完成后,按模板顺序依次执行mounted,最后父组件才进入mounted钩子。
更新阶段:
- 更新数据时,父组件先进入
beforeUpdate钩子,然后子组件按模板顺序依次进入beforeUpdate。 - 子组件完成更新后依次执行
updated钩子,最后父组件执行updated钩子。
销毁阶段:
- 父组件的
beforeDestroy钩子执行后,子组件按模板顺序依次执行beforeDestroy和destroyed。 - 最后,父组件执行
destroyed钩子。
使用 keep-alive 包裹的子组件时新增的生命周期钩子
在 Vue 中使用 keep-alive 组件时,Vue 会缓存被包裹的组件实例,而不是销毁它们。这会导致在某些生命周期阶段,组件的行为有所不同。
具体来说,keep-alive 会为包裹的组件添加两个新的生命周期钩子:activated 和 deactivated。
1. activated
当被 keep-alive 缓存的组件被激活时调用。这通常发生在组件从 inactive 状态恢复时,比如当其被重新显示出来时。
activated() {
console.log('Component activated');
}
2. deactivated
当组件被 keep-alive 缓存时,且该组件被激活的父组件或视图被切换时,这个钩子会在组件被“去激活”(即被隐藏)时调用。
deactivated() {
console.log('Component deactivated');
}
keep-alive 包裹的子组件生命周期执行情况
activated:当组件从 keep-alive 缓存中恢复并重新进入视图时,activated 钩子会被调用。这通常发生在路由导航切换时,或者通过某些其他条件控制组件显示的场景。
deactivated:当组件被隐藏或切换离开视图时,deactivated 钩子会被触发。此时,组件不会被销毁,而是保持在缓存中。再次进入视图时,activated 钩子会再次调用。
比如这里我们在子组件 MyList 中:
<template>
<div class="my-list">
这里是子组件 MyList 的内容,父组件传来的 msg: {{ msg }}
</div>
</template>
<script>
export default {
name: "MyList",
props: {
msg: {
type: String,
required: true,
},
},
activated() {
// 组件被激活时
console.log("activated: 子组件 MyList 被激活");
},
deactivated() {
// 组件被取消激活时
console.log("deactivated: 子组件 MyList 被取消激活");
},
beforeCreate() {
console.log("子组件 MyList:beforeCreate");
},
created() {
console.log("子组件 MyList:created");
},
beforeMount() {
console.log("子组件 MyList:beforeMount");
},
mounted() {
console.log("子组件 MyList:mounted");
},
beforeUpdate() {
console.log("子组件 MyList:beforeUpdate");
},
updated() {
console.log("子组件 MyList:updated");
},
beforeDestroy() {
console.log("子组件 MyList:beforeDestroy");
},
destroyed() {
console.log("子组件 MyList:destroyed");
},
};
</script>
<style scoped></style>
然后在父组件使用 keep-alive 进行包裹,并且为子组件增加 v-if 去控制子组件的显示和隐藏,便于我们触发 activated 和 deactivated 钩子:
<template>
<div id="app">
<button @click="change">更新数据</button>
<button @click="destroyComponent">销毁组件</button>
<button @click="isComponentVisible = !isComponentVisible">
显示/隐藏子组件
</button>
<p>{{ msg }}</p>
<hr />
<keep-alive> <my-list v-if="isComponentVisible" :msg="msg"/> </keep-alive>
</div>
</template>
<script>
import MyList from "@/components/MyList.vue";
export default {
name: "App",
components: { MyList },
data() {
return {
msg: "Hello World",
isComponentVisible: true, // 控制子组件的显示和隐藏
};
},
// ...这里我们省略第一节示例中的生命周期部分代码
methods: {
change() {
// 更新数据
this.msg = "msg 被我修改啦!";
},
destroyComponent() {
// 销毁组件
this.$destroy();
},
},
};
</script>
<style></style>
第一次打开控制台时
我们可以发现子组件的 activated 生命周期钩子在子组件的 mounted 生命周期钩子之后执行。这是因为在 keep-alive 缓存机制中,组件的生命周期钩子执行顺序有特殊规则。
第一次渲染时,
mounted钩子会在组件完成初始挂载后执行。此时组件真正被插入到 DOM 中,显示在页面上。之后,activated钩子会紧接着mounted钩子执行,标识该组件进入了活跃状态。
当点击按钮使子组件隐藏时
此时当点击按钮使子组件被隐藏(isComponentVisible 设为 false)时,我们会看到 deactivated 钩子被触发,而不是 destroyed 钩子。这是因为 keep-alive 机制将组件缓存起来,而不是销毁它。
同时,由于 isComponentVisible 发生了变化,会触发父组件的 更新 生命周期钩子。所以父组件的 beforeUpdate 和 updated 钩子将会执行,因为父组件的状态发生了改变,从而需要重新渲染 DOM。
所以在这种情况下,生命周期钩子的执行顺序如下:
父组件的
beforeUpdate:父组件在检测到 isComponentVisible 变化后,进入更新过程。子组件的
deactivated:由于 keep-alive 机制的作用,子组件从活跃状态切换为非活跃状态,触发 deactivated 钩子,而不执行 destroyed 钩子。父组件的
updated:父组件完成更新后执行 updated 钩子,标志更新过程结束。
再次点击按钮使子组件激活时
当子组件重新被激活时(即再次点击按钮将 isComponentVisible 设为 true),我们会看到子组件的 activated 被触发,而不是 created 或 mounted 钩子。这是因为 keep-alive 机制在缓存中保留了组件的状态,直接恢复了其活跃性,而无需重新初始化或挂载。
同时,由于 isComponentVisible 发生了变化,会触发父组件的 更新 生命周期钩子。所以父组件的 beforeUpdate 和 updated 钩子将会执行,因为父组件的状态发生了改变,从而需要重新渲染 DOM。
此时的生命周期钩子执行顺序如下:
父组件的
beforeUpdate:父组件检测到isComponentVisible的值变化,进入更新阶段。子组件的
activated:子组件恢复到活跃状态,执行activated钩子,而不重新执行created或mounted钩子。父组件的
updated:父组件完成更新后,触发updated钩子,标志此次更新过程结束。
因此,每当我们使用 keep-alive 缓存组件切换显示状态,Vue 会通过 activated 和 deactivated 钩子来控制组件的状态,而不会反复销毁和挂载组件。这种机制不仅优化了性能,也在多页面导航或动态组件切换的场景中提供了稳定性。
keep-alive 钩子执行顺序小结
首次渲染:beforeCreate -> created -> beforeMount -> mounted -> activated
从视图中移除 :直接执行 deactivated
重新激活:直接执行 activated
通过这种方式,keep-alive 确保了组件在频繁切换时保持状态、减少资源消耗,同时提供灵活的钩子控制组件的激活和缓存状态。