Vue父子元件生命週期執行順序及鉤子函式的個人理解

餘大彬發表於2018-08-11

先附一張官網上的vue例項的生命週期圖,每個Vue例項在被建立的時候都需要經過一系列的初始化過程,例如需要設定資料監聽,編譯模板,將例項掛載到DOM並在資料變化時更新DOM等。同時在這個過程中也會執行一些叫做生命週期鉤子的函式(回撥函式),這給了使用者在不同階段新增自己程式碼的機會。

1、vue的生命週期圖

 

在vue例項的整個生命週期的各個階段,會提供不同的鉤子函式以供我們進行不同的操作。先列出vue官網上對各個鉤子函式的詳細解析。

生命週期鉤子    

詳細
beforeCreate 在例項初始化之後,資料觀測(data observer) 和 event/watcher 事件配置之前被呼叫。
created

例項已經建立完成之後被呼叫。在這一步,例項已完成以下的配置:資料觀測(data observer),屬性和方法的運算, watch/event 事件回撥。

在執行data()方法前props屬性有資料已經可以訪問,watch和computed監聽函式此時為null,此時this.computed裡的計算屬性值為undefined。data函式執行完後,watch和computed監聽函式才可用,因為data函式執行完後,data函式return的屬性這時才可用。然而,掛載階段還沒開始,$el 屬性目前不可見。

beforeMount 在掛載開始之前被呼叫:相關的 render 函式首次被呼叫。
mounted el 被新建立的 vm.$el 替換,並掛載到例項上去之後呼叫該鉤子。如果 root 例項掛載了一個文件內元素,當 mounted 被呼叫時 vm.$el 也在文件內。
beforeUpdate 資料更新時呼叫,發生在虛擬 DOM 重新渲染和打補丁之前。你可以在這個鉤子中進一步地更改狀態,這不會觸發附加的重渲染過程。
updated 由於資料更改導致的虛擬 DOM 重新渲染和打補丁,在這之後會呼叫該鉤子。當這個鉤子被呼叫時,元件 DOM 已經更新,所以你現在可以執行依賴於 DOM 的操作。
activated keep-alive 元件啟用時呼叫。
deactivated keep-alive 元件停用時呼叫。
beforeDestroy 例項銷燬之前呼叫。在這一步,例項仍然完全可用。
destroyed Vue 例項銷燬後呼叫。呼叫後,Vue 例項指示的所有東西都會解繫結,所有的事件監聽器會被移除,所有的子例項也會被銷燬。

2、實際操作 

下面我們在實際的程式碼執行過程中理解父子元件生命週期建立過程以及鉤子函式執行的實時狀態變化。

測試基於下面的程式碼,引入vue.js檔案後即可執行。(開啟頁面後,再按一次重新整理會自動進入debugger狀態)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        
    </style>
</head>   
<body>
<div id="app">
    <p>{{message}}</p>
    <keep-alive>
        <my-components :msg="msg1" v-if="show"></my-components>
    </keep-alive>
</div>
</body>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
    var child = {
        template: '<div>from child: {{childMsg}}</div>',
        props: ['msg'],
        data: function() {
            return {
                childMsg: 'child'
            }   
        },
        beforeCreate: function () {
            debugger;
        },
        created: function () {
            debugger;
        },
        beforeMount: function () {
            debugger;
        },
        mounted: function () {
            debugger;
        },
        deactivated: function(){
            alert("keepAlive停用");
        },
        activated: function () {
            console.log('component activated');
        },
        beforeDestroy: function () {
            console.group('beforeDestroy 銷燬前狀態===============》');
            var state = {
                'el': this.$el,
                'data': this.$data,
                'message': this.message
            }
            console.log(this.$el);
            console.log(state);
        },
        destroyed: function () {
            console.group('destroyed 銷燬完成狀態===============》');
            var state = {
                'el': this.$el,
                'data': this.$data,
                'message': this.message
            }
            console.log(this.$el);
            console.log(state);
        },
    };
    var vm = new Vue({
        el: '#app',
        data: {
                message: 'father',
                msg1: "hello",
                show: true
            },
        beforeCreate: function () {
            debugger;
        },
        created: function () {
            debugger;
        },
        beforeMount: function () {
            debugger;
        },
        mounted: function () {
            debugger;    
        },
        beforeUpdate: function () {
            alert("頁面檢視更新前");
            
        },
        updated: function () {
            alert("頁面檢視更新後");
        },
        beforeDestroy: function () {
            console.group('beforeDestroy 銷燬前狀態===============》');
            var state = {
                'el': this.$el,
                'data': this.$data,
                'message': this.message
            }
            console.log(this.$el);
            console.log(state);
        },
        destroyed: function () {
            console.group('destroyed 銷燬完成狀態===============》');
            var state = {
                'el': this.$el,
                'data': this.$data,
                'message': this.message
            }
            console.log(this.$el);
            console.log(state);
        },
        components: {
            'my-components': child
        }
    });
</script>
</html>

3.1、生命週期除錯

首先我們建立了一個Vue例項vm,將其掛載到頁面中id為“app”的元素上。

3.1.1、根元件的beforeCreate階段

可以看出,在呼叫beforeCreate()函式時,只進行了一些必要的初始化操作(例如一些全域性的配置和根例項的一些屬性初始化),此時data屬性為undefined,沒有可供操作的資料。

3.1.2、根元件的Created階段

呼叫Created()函式,在這一步,例項已完成以下的配置:資料代理和動態資料繫結(data observer),屬性和方法的運算, watch/event 事件回撥。然而,掛載階段還沒開始,$el 屬性目前不可見。

3.1.3、根元件的beforeMount階段

在呼叫boforeMount()函式前首先會判斷物件是否有el選項。如果有的話就繼續向下編譯,如果沒有el選項,則停止編譯,也就意味著停止了生命週期,直到在該vue例項上呼叫vm.$mount(el)

在這個例子中,我們有el元素,因此會呼叫boforeMount()函式,此時已經開始執行模板解析函式,但還沒有將$el元素掛載頁面,頁面檢視因此也未更新。在標紅處,還是 {{message}},這裡就是應用的 Virtual DOM(虛擬Dom)技術,先把坑佔住了。到後面mounted掛載的時候再把值渲染進去。

3.1.4、子元件的beforeCreate、Created、beforeMount、Mounted階段

在父元件執行beforeMount階段後,進入子元件的beforeCreate、Created、beforeMount階段,這些階段和父元件類似,按下不表。beforeMount階段後,執行的是Mounted階段,該階段時子元件已經掛載到父元件上,並且父元件隨之掛載到頁面中。

由下圖可以知道,在beforeMount階段之後、Mounted階段之前,資料已經被載入到檢視上了,即$el元素被掛載到頁面時觸發了檢視的更新

3.1.5、子元件的activated階段

 我們發現在子父元件全部掛載到頁面之後被觸發。這是因為子元件my-components被<keep-alive> 包裹,隨$el的掛載被觸發。如果子元件沒有被<keep-alive>包裹,那麼該階段將不會被觸發。

3.1.6、父元件的mounted階段

mounted執行時:此時el已經渲染完成並掛載到例項上。

至此,從Vue例項的初始化到將新的模板掛載到頁面上的階段已經完成,退出debugger。下面我們來看一下deactivated、beforeUpdate、updated、beforeDestroy、destroyed鉤子函式。

3.2、deactivated、beforeUpdate、updated階段

由生命週期函式可知:當資料變化後、虛擬DOM渲染重新渲染頁面前會觸發beforeUpdate()函式,此時檢視還未改變。當虛擬DOM渲染頁面檢視更新後會觸發updated()函式。

 

我們不妨改變vm.show = false,當修改這個屬性時,不僅會觸發beforeUpdate、updated函式,還會觸發deactivated函式(因為keep-alive 元件停用時呼叫)。我們不妨想一下deactivated函式會在beforeUpdate後還是updated後呼叫。

我們在控制檯輸入vm.show = false。得到三者的呼叫順序分別為beforeUpdate、deactivated、updated。我們可以知道的是deactivated函式的觸發時間是在檢視更新時觸發。因為當檢視更新時才能知道keep-alive元件被停用了。

3.3、beforeDestroy和destroyed鉤子函式間的生命週期

現在我們對Vue例項進行銷燬,呼叫app.$destroy()方法即可將其銷燬,控制檯測試如下:

 

我們發現例項依然存在,但是此時變化已經發生在了其他地方。

beforeDestroy鉤子函式在例項銷燬之前呼叫。在這一步,例項仍然完全可用。

destroyed鉤子函式在Vue 例項銷燬後呼叫。呼叫後,Vue 例項指示的所有東西都會解繫結,所有的事件監聽器會被移除,所有的子例項也會被銷燬(也就是說子元件也會觸發相應的函式)。這裡的銷燬並不指代'抹去',而是表示'解綁'。

銷燬時beforeDestory函式的傳遞順序為由父到子,destory的傳遞順序為由子到父。

4、一些應用鉤子函式的想法

  • 在created鉤子中可以對data資料進行操作,這個時候可以進行ajax請求將返回的資料賦給data。
  • 雖然updated函式會在資料變化時被觸發,但卻不能準確的判斷是那個屬性值被改變,所以在實際情況中用computed或match函式來監聽屬性的變化,並做一些其他的操作。
  • 在mounted鉤子對掛載的dom進行操作,此時,DOM已經被渲染到頁面上。
  • 在使用vue-router時有時需要使用<keep-alive></keep-alive>來快取元件狀態,這個時候created鉤子就不會被重複呼叫了,如果我們的子元件需要在每次載入或切換狀態的時候進行某些操作,可以使用activated鉤子觸發。
  • 所有的生命週期鉤子自動繫結 this 上下文到例項中,所以不能使用箭頭函式來定義一個生命週期方法 (例如 created: () => this.fetchTodos())。這是導致this指向父級。

5、 小結

  • 載入渲染過程

  父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

  • 子元件更新過程

  父beforeUpdate->子beforeUpdate->子updated->父updated

  • 父元件更新過程

  父beforeUpdate->父updated

  • 銷燬過程

  父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

6、參考文章

關於Vue.js2.0生命週期的研究與理解

Vue2.0 探索之路——生命週期和鉤子函式的一些理解

 

PS:如果文章對您有一些幫助,歡迎點選推薦,您的推薦是我不斷輸出的動力!

相關文章