Vue 生命周期是每個 Vue 開發者必須深入理解的核心概念之一。它定義了組件從創建、掛載、更新、銷毀的整個過程,以及在這個過程中各個階段提供的鉤子函數。掌握生命周期不僅能幫助你理解 Vue 的工作原理,還能讓你在合適的時機執行特定的操作,優化應用性能,避免常見陷阱。本文將從源碼實現到實際應用,全面解析 Vue 生命周期的各個階段。
一、生命周期概覽
Vue 組件的生命周期可以分為四個主要階段:
- 初始化與掛載:創建組件實例,初始化數據,掛載 DOM
- 數據更新:響應式數據變更時觸發更新流程
- 銷毀:清理組件并釋放資源
- 特殊場景:錯誤處理、服務器端渲染等
每個階段都提供了相應的鉤子函數,開發者可以在這些鉤子中注入自定義邏輯。生命周期鉤子的執行順序是固定的,理解這個順序對于編寫正確的代碼至關重要。
二、生命周期階段詳解
2.1 初始化與掛載階段
這個階段從組件實例創建開始,到 DOM 掛載完成結束。
2.1.1 beforeCreate
- 觸發時機:實例初始化之后,數據觀測 (data observer) 和
event/watcher
事件配置之前被調用。 - 特性:
- 此時
this
指向實例,但數據和方法均未初始化。 - 無法訪問
data
、methods
或computed
。 - 通常用于初始化非響應式數據或全局插件。
- 此時
- 示例:
export default {beforeCreate() {// 初始化全局事件總線this.$bus = new Vue();// 記錄組件創建時間this._createdAt = Date.now();} }
2.1.2 created
- 觸發時機:實例已經創建完成之后被調用。在這一步,實例已經完成了數據觀測、
property
和method
的計算、watch/event
事件回調的配置。然而,掛載階段還沒有開始,$el
屬性目前不可用。 - 特性:
- 可以訪問
data
、methods
和computed
,但 DOM 尚未掛載。 - 適合進行數據獲取(如 API 請求)或初始化依賴數據的操作。
- 可以訪問
- 示例:
export default {data() {return {users: []};},async created() {try {const response = await fetch('/api/users');this.users = await response.json();} catch (error) {console.error('Failed to fetch users', error);}} }
2.1.3 beforeMount
- 觸發時機:掛載開始之前被調用。
- 特性:
- 模板編譯/渲染函數已經完成,但尚未掛載到 DOM。
$el
是虛擬 DOM,不可訪問實際 DOM 元素。- 適合在渲染前對模板進行最后的修改。
- 源碼關鍵點:
// Vue 源碼簡化版 vm.$el = vm.$options.el; callHook(vm, 'beforeMount');// 編譯模板生成 render 函數 const updateComponent = () => {vm._update(vm._render(), hydrating); };
2.1.4 mounted
- 觸發時機:掛載完成后被調用。此時模板已經編譯完成并掛載到 DOM 上。
- 特性:
- 可以訪問
$el
和實際 DOM 元素。 - 子組件已經完成掛載(但不保證所有異步子組件都已完成)。
- 適合進行 DOM 操作、初始化第三方插件(如 Chart.js、Leaflet)或訂閱事件。
- 可以訪問
- 示例:
export default {mounted() {// 初始化圖表this.chart = new Chart(this.$el.querySelector('#chart'), {type: 'bar',data: this.chartData});// 添加 DOM 事件監聽器this.$el.addEventListener('click', this.handleClick);},beforeDestroy() {// 清理圖表實例和事件監聽器this.chart.destroy();this.$el.removeEventListener('click', this.handleClick);} }
2.2 數據更新階段
這個階段在組件數據發生變化時觸發,包含虛擬 DOM 重新渲染和打補丁的過程。
2.2.1 beforeUpdate
- 觸發時機:數據更新時調用,發生在虛擬 DOM 打補丁之前。
- 特性:
- 數據已經變更,但 DOM 尚未更新。
- 可以訪問更新前的 DOM 狀態。
- 適合在更新前保存當前 DOM 狀態或執行一些預處理。
- 示例:
export default {data() {return {list: [1, 2, 3]};},beforeUpdate() {// 保存更新前的列表高度this.prevListHeight = this.$el.offsetHeight;} }
2.2.2 updated
- 觸發時機:由于數據更改導致的虛擬 DOM 重新渲染和打補丁之后調用。
- 特性:
- 數據和 DOM 都已經更新。
- 可以訪問更新后的 DOM 狀態。
- 注意:不要在這個鉤子中修改數據,否則可能導致無限循環更新。
- 示例:
export default {updated() {// 對比更新前后的列表高度,執行動畫if (this.prevListHeight !== this.$el.offsetHeight) {this.animateListHeightChange();}} }
2.3 銷毀階段
這個階段在組件實例銷毀時觸發,用于清理資源和事件監聽器。
2.3.1 beforeDestroy (Vue 2) / beforeUnmount (Vue 3)
- 觸發時機:實例銷毀之前調用。此時實例仍然完全可用。
- 特性:
- 組件仍然完全正常工作。
- 適合進行資源清理(如定時器、事件監聽器、WebSocket 連接等)。
- 示例:
export default {created() {this.timer = setInterval(() => {console.log('定時任務');}, 1000);this.$bus.$on('some-event', this.handleEvent);},beforeDestroy() {// 清理定時器clearInterval(this.timer);// 取消事件訂閱this.$bus.$off('some-event', this.handleEvent);} }
2.3.2 destroyed (Vue 2) / unmounted (Vue 3)
- 觸發時機:實例已經完全銷毀之后調用。
- 特性:
- 所有的事件監聽器和子實例已經被銷毀。
- 組件實例完全不可用。
- 通常用于確認資源是否已經正確釋放。
- 源碼關鍵點:
// Vue 源碼簡化版 callHook(vm, 'beforeDestroy');// 遞歸銷毀子組件 vm.$children.forEach(child => {child.$destroy(); });// 移除所有事件監聽器 vm._events = Object.create(null);callHook(vm, 'destroyed');
2.4 特殊場景鉤子
2.4.1 activated / deactivated
- 觸發時機:
activated
:被<keep-alive>
緩存的組件激活時調用。deactivated
:被<keep-alive>
緩存的組件停用時調用。
- 特性:
- 只在使用
<keep-alive>
包裹的組件中觸發。 - 適合處理緩存組件的特殊邏輯(如恢復滾動位置、刷新數據)。
- 只在使用
- 示例:
<keep-alive><router-view /> </keep-alive>
export default {activated() {// 組件被激活時刷新數據this.fetchData();},deactivated() {// 保存組件狀態this.saveScrollPosition();} }
2.4.2 errorCaptured (Vue 2) / errorCaptured + renderTracked + renderTriggered (Vue 3)
- 觸發時機:
errorCaptured
:捕獲來自子孫組件的錯誤時調用。renderTracked
/renderTriggered
(Vue 3):用于調試響應式依賴的追蹤和觸發。
- 特性:
- 可以阻止錯誤繼續向上傳播。
- 適合實現全局錯誤處理或日志記錄。
- 示例:
export default {errorCaptured(err, vm, info) {// 記錄錯誤日志console.error('Error captured:', err, info);// 可以返回 false 阻止錯誤繼續向上傳播return false;} }
2.4.3 serverPrefetch (Vue 3 僅 SSR)
- 觸發時機:在服務器端渲染(SSR)期間,組件實例在服務器上被創建時調用。
- 特性:
- 僅在 SSR 模式下有效。
- 用于在服務器端預取數據,避免客戶端重復請求。
- 示例:
export default {async serverPrefetch() {// 在服務器端預取數據this.data = await fetchData();} }
三、生命周期流程圖與執行順序
3.1 Vue 2 生命周期流程圖
創建實例↓
beforeCreate↓
初始化 data/methods↓
created↓
是否有 el 選項?↓├─ 否 → 等待 vm.$mount(el)↓├─ 是 → 是否有 template 選項?↓├─ 是 → 編譯 template 為 render 函數↓└─ 否 → 使用 el 的 outerHTML 作為 template↓
beforeMount↓
創建 vm.$el 并替換 el↓
mounted↓
數據變更↓
beforeUpdate↓
虛擬 DOM 重新渲染 & 打補丁↓
updated↓
調用 vm.$destroy()↓
beforeDestroy↓
銷毀所有子實例、事件監聽器和子組件↓
destroyed
3.2 Vue 3 生命周期變更
Vue 3 對生命周期鉤子進行了一些重命名,以更準確地反映其用途:
beforeDestroy
→beforeUnmount
destroyed
→unmounted
新增鉤子:
setup()
:替代beforeCreate
和created
,是 Composition API 的入口點。renderTracked
/renderTriggered
:用于調試響應式依賴。
四、生命周期鉤子的實際應用場景
4.1 數據獲取
- 最佳位置:
created
或mounted
- 選擇依據:
- 如果數據獲取不依賴 DOM 操作,使用
created
(稍早執行)。 - 如果需要訪問 DOM 元素,使用
mounted
。
- 如果數據獲取不依賴 DOM 操作,使用
- 示例:
export default {data() {return {posts: [],loading: true,error: null};},async created() {try {const response = await fetch('/api/posts');this.posts = await response.json();} catch (error) {this.error = error.message;} finally {this.loading = false;}} }
4.2 DOM 操作與第三方插件集成
- 最佳位置:
mounted
- 示例:初始化 Chart.js 圖表
export default {mounted() {const ctx = this.$el.querySelector('#myChart').getContext('2d');this.chart = new Chart(ctx, {type: 'line',data: this.chartData,options: this.chartOptions});},beforeUnmount() {// 銷毀圖表實例this.chart.destroy();} }
4.3 狀態恢復與保存
- 最佳位置:
activated
/deactivated
(配合<keep-alive>
) - 示例:保存和恢復滾動位置
export default {data() {return {scrollPosition: 0};},deactivated() {// 保存當前滾動位置this.scrollPosition = window.scrollY;},activated() {// 恢復滾動位置window.scrollTo(0, this.scrollPosition);} }
4.4 資源清理
- 最佳位置:
beforeDestroy
/beforeUnmount
- 示例:清理定時器、取消訂閱、關閉網絡連接
export default {created() {this.socket = new WebSocket('ws://example.com');this.interval = setInterval(this.updateData, 5000);},beforeUnmount() {// 清理 WebSocket 連接this.socket.close();// 清除定時器clearInterval(this.interval);} }
4.5 全局狀態初始化
- 最佳位置:
beforeCreate
- 示例:初始化全局事件總線或配置
export default {beforeCreate() {// 初始化全局事件總線this.$bus = new Vue();// 配置全局 API 基地址this.$apiBaseUrl = process.env.VUE_APP_API_BASE_URL;} }
五、生命周期鉤子的性能考慮
5.1 避免在鉤子中執行耗時操作
- 問題:在
mounted
或updated
等鉤子中執行大量計算或同步 API 請求會阻塞 UI 渲染。 - 解決方案:
- 使用異步操作(如
async/await
)處理 API 請求。 - 將復雜計算移至
computed
屬性或watch
中。
export default {async mounted() {// 錯誤:同步執行大量計算// this.result = heavyCalculation(this.data);// 正確:異步執行setTimeout(() => {this.result = heavyCalculation(this.data);}, 0);// 或使用 Web Workerthis.worker.postMessage(this.data);this.worker.onmessage = (e) => {this.result = e.data;};} }
- 使用異步操作(如
5.2 避免在 updated 中修改數據
- 問題:在
updated
中修改數據會觸發新的更新周期,可能導致無限循環。 - 解決方案:
- 僅在數據滿足特定條件時才修改,且確保不會再次觸發更新。
export default {updated() {// 錯誤:可能導致無限循環// if (this.value < 10) this.value++;// 正確:使用 nextTick 避免立即觸發更新if (this.value < 10 && !this.updating) {this.updating = true;this.$nextTick(() => {this.value++;this.updating = false;});}} }
5.3 合理使用生命周期鉤子
- 問題:在不需要的鉤子中添加邏輯會增加組件復雜度和執行時間。
- 解決方案:
- 只在真正需要的鉤子中添加代碼。
- 使用 Composition API 將相關邏輯組織在一起,減少對生命周期鉤子的依賴。
六、Vue 3 Composition API 中的生命周期
Vue 3 的 Composition API 提供了與生命周期鉤子等效的函數,使邏輯復用更加靈活:
6.1 等效鉤子映射
選項式 API | Composition API |
---|---|
beforeCreate | setup() |
created | setup() |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked (僅開發模式) |
renderTriggered | onRenderTriggered (僅開發模式) |
6.2 示例:使用 Composition API 訪問生命周期
import { onMounted, onBeforeUnmount, ref } from 'vue';export default {setup() {const count = ref(0);let timer;// 等效于 mountedonMounted(() => {timer = setInterval(() => {count.value++;}, 1000);});// 等效于 beforeUnmountonBeforeUnmount(() => {clearInterval(timer);});return {count};}
};
6.3 Composition API 的優勢
- 邏輯復用:可以將相關生命周期邏輯封裝到可復用的函數中。
- 代碼組織:將同一功能的代碼集中在一起,提高可讀性。
- 類型安全:更好地支持 TypeScript,提供更準確的類型推導。
七、生命周期鉤子的常見誤區與解決方案
7.1 誤區:在 mounted 中直接操作子組件 DOM
- 問題:子組件可能尚未完全掛載,直接訪問子組件 ref 會失敗。
- 解決方案:
- 使用
nextTick
確保子組件已掛載。 - 使用事件或 props 進行組件間通信。
export default {mounted() {// 錯誤:子組件可能尚未掛載// this.$refs.child.doSomething();// 正確:使用 nextTickthis.$nextTick(() => {this.$refs.child.doSomething();});} }
- 使用
7.2 誤區:在 destroyed 中訪問組件實例
- 問題:在
destroyed
鉤子中,組件實例已經完全銷毀,訪問this
可能導致錯誤。 - 解決方案:
- 在
beforeDestroy
中進行所有清理操作。
export default {beforeDestroy() {// 正確:此時組件仍然可用this.cleanupResources();},destroyed() {// 錯誤:不要在這里訪問 this// this.cleanupResources(); // 可能導致錯誤} }
- 在
7.3 誤區:過度使用生命周期鉤子
- 問題:在多個生命周期鉤子中重復相同的邏輯,導致代碼冗余。
- 解決方案:
- 使用 Composition API 將相關邏輯封裝到一個函數中。
- 使用
watch
或computed
處理數據變化邏輯。
// 壞:重復邏輯 export default {mounted() {this.initData();},activated() {this.initData();},methods: {initData() {// 初始化邏輯}} };// 好:使用 Composition API 封裝 import { onMounted, onActivated } from 'vue';export function useInitData(initFn) {onMounted(initFn);onActivated(initFn); }// 在組件中使用 export default {setup() {useInitData(() => {// 初始化邏輯});} };
八、生命周期源碼解析(簡化版)
Vue 的生命周期實現主要涉及以下幾個核心模塊:
- 實例初始化:
src/core/instance/init.js
- 生命周期鉤子:
src/core/instance/lifecycle.js
- 渲染流程:
src/core/instance/render.js
- 更新流程:
src/core/observer/watcher.js
8.1 關鍵源碼片段
// src/core/instance/init.js
Vue.prototype._init = function(options) {const vm = this;// 初始化生命周期狀態initLifecycle(vm);// 初始化事件系統initEvents(vm);// 初始化渲染initRender(vm);// 調用 beforeCreate 鉤子callHook(vm, 'beforeCreate');// 初始化注入initInjections(vm);// 初始化 data、props、computed 等initState(vm);// 初始化 provideinitProvide(vm);// 調用 created 鉤子callHook(vm, 'created');if (vm.$options.el) {// 掛載組件vm.$mount(vm.$options.el);}
};// src/core/instance/lifecycle.js
Vue.prototype.$mount = function(el) {// 編譯模板并生成 render 函數const mount = Vue.prototype._mount;const vm = this;// 調用 beforeMount 鉤子callHook(vm, 'beforeMount');// 執行渲染const vnode = vm._render();vm._update(vnode, hydrating);// 調用 mounted 鉤子callHook(vm, 'mounted');return vm;
};// 數據更新觸發的更新流程
Watcher.prototype.update = function() {const vm = this.vm;// 調用 beforeUpdate 鉤子callHook(vm, 'beforeUpdate');// 執行虛擬 DOM 更新vm._update(vm._render(), hydrating);// 調用 updated 鉤子callHook(vm, 'updated');
};// 組件銷毀流程
Vue.prototype.$destroy = function() {const vm = this;// 調用 beforeDestroy 鉤子callHook(vm, 'beforeDestroy');// 執行銷毀操作:移除事件監聽器、銷毀子組件等vm._isBeingDestroyed = true;// 遞歸銷毀子組件vm.$children.forEach(child => {child.$destroy();});// 移除所有事件監聽器vm._events = Object.create(null);// 調用 destroyed 鉤子callHook(vm, 'destroyed');vm._isDestroyed = true;
};
九、總結與最佳實踐
9.1 生命周期關鍵要點總結
-
初始化與掛載:
beforeCreate
:實例初始化后,數據和事件系統尚未初始化。created
:數據觀測、property
和method
計算完成,可進行數據獲取。beforeMount
:模板編譯完成,但尚未掛載到 DOM。mounted
:DOM 掛載完成,可進行 DOM 操作和第三方插件初始化。
-
數據更新:
beforeUpdate
:數據變更后,DOM 更新前。updated
:DOM 更新完成,避免在此修改數據。
-
銷毀階段:
beforeDestroy
/beforeUnmount
:實例銷毀前,可進行資源清理。destroyed
/unmounted
:實例完全銷毀,不可訪問組件狀態。
-
特殊場景:
activated
/deactivated
:<keep-alive>
緩存組件的激活/停用。errorCaptured
:捕獲子孫組件的錯誤。
9.2 最佳實踐建議
- 數據獲取:優先在
created
中進行,避免阻塞 DOM 渲染。 - DOM 操作:僅在
mounted
或之后進行,確保 DOM 已渲染。 - 資源清理:在
beforeDestroy
/beforeUnmount
中清理定時器、事件監聽器等。 - 避免重復邏輯:使用 Composition API 或 mixins 復用生命周期相關邏輯。
- 調試工具:利用 Vue DevTools 監控生命周期鉤子的執行情況。
- 性能優化:避免在生命周期鉤子中執行耗時操作,使用異步處理。
掌握 Vue 生命周期是成為一名優秀 Vue 開發者的基礎。通過合理利用生命周期鉤子,你可以更精確地控制組件行為,優化應用性能,避免常見的開發陷阱。在實際開發中,結合 Composition API 的強大功能,你可以更靈活地組織和復用代碼,打造出高質量的 Vue 應用。