vue是每一個前端開發人員都繞不過的一個技術,在國內的市場占有量也是非常的大,我們大部分人用著vue, 卻不知道他內部其實經歷了一些什么,每個生命周期又是什么時候開始執行的,我們今天來詳細的看一看
首先,生命周期是個啥?
借用官網的一句話就是:每一個vue實體從創建到銷毀的程序,就是這個vue實體的生命周期,在這個程序中,他經歷了從開始創建、初始化資料、編譯模板、掛載Dom、渲染→更新→渲染、卸載等一系列程序,那么這些程序中,具體vue做了些啥,我們今天來了解一下,
語述
了解之前,我們先貼上一張官網的生命周期圖,從圖上,我們再一步一步來理解vue生命周期,

我們先簡單的來解說這張圖,然后再通過例子來詳看
首先,從圖上,我們可以看出,他的一個程序是
- new Vue()實體化一個vue實體,然后init初始化event 和 lifecycle, 其實這個程序中分別呼叫了3個初始化函式(initLifecycle(), initEvents(), initRender()),分別初始化了生命周期,事件以及定義createElement函式,初始化生命周期時,定義了一些屬性,比如表示當前狀態生命周期狀態得_isMounted ,_isDestroyed ,_isBeingDestroyed,表示keep-alive中組件狀態的_inactive,而初始化event時,實際上就是定義了$once、$off、$emit、$on幾個函式,而createElement函式是在初始化render時定義的(呼叫了initRender函式)
- 執行beforeCreate生命周期函式
- beforeCreate執行完后,會開始進行資料初始化,這個程序,會定義data資料,方法以及事件,并且完成資料劫持observe以及給組件實體配置watcher觀察者實體,這樣,后續當資料發生變化時,才能感知到資料的變化并完成頁面的渲染
- 執行created生命周期函式,所以,當這個函式執行的時候,我們已經可以拿到data下的資料以及methods下的方法了,所以在這里,我們可以開始呼叫方法進行資料請求了
- created執行完后,我們可以看到,這里有個判斷,判斷當前是否有el引數(這里為什么需要判斷,是因為我們后面的操作是會依賴這個el的,后面會詳細說),如果有,我們再看是否有template引數,如果沒有el,那么我們會等待呼叫$mount(el)方法(后面會詳細說),
- 確保有了el后,繼續往下走,判斷當有template引數時,我們會選擇去將template模板轉換成render函式(其實在這前面是還有一個判斷的,判斷當前是否有render函式,如果有的話,則會直接去渲染當前的render函式,如果沒有那么我們才開始去查找是否有template模板),如果沒有template,那么我們就會直接將獲取到的el(也就是我們常見的#app,#app里面可能還會有其他標簽)編譯成templae, 然后在將這個template轉換成render函式,
- 之后再呼叫beforMount, 也就是說實際從creted到beforeMount之間,最主要的作業就是將模板或者el轉換為render函式,并且我們可以看出一點,就是你不管是用el,還是用template, 或者是用我們最常用的.vue檔案(如果是.vue檔案,他其實是會先編譯成為template),最終他都是會被轉換為render函式的,
- beforeMount呼叫后,我們是不是要開始渲染render函式了,首先我們會先生產一個虛擬dom(用于后續資料發生變化時,新老虛擬dom對比計算),進行保存,然后再開始將render渲染成為真實的dom,渲染成真實dom后,會將渲染出來的真實dom替換掉原來的vm.$el(這一步我們可能不理解,請耐心往下看,后面我會舉例說明),然后再將替換后的$el append到我們的頁面內,整個初步流程就算是走完了
- 之后再呼叫mounted,并將標識生命周期的一個屬性_isMounted 置為true,所以mounted函式內,我們是可以操作dom的,因為這個時候dom已經渲染完成了,
- 再之后,只有當我們狀態資料發生變化時,我們在觸發beforeUpdate,要開始將我們變化后的資料渲染到頁面上了(實際上這里是有個判斷的,判斷當前的_isMounted是不是為ture并且_isDestroyed是不是為false,也就是說,保證dom已經被掛載的情況下,且當前組件并未被銷毀,才會走update流程)
- beforeUpdate呼叫之后,我們又會重新生成一個新的虛擬dom(Vnode),然后會拿這個最新的Vnode和原來的Vnode去做一個diff算,這里就涉及到一系列的計算,算出最小的更新范圍,從而更新render函式中的最新資料,再將更新后的render函式渲染成真實dom,也就完成了我們的資料更新
- 然后再執行updated,所以updated里面也可以操作dom,并拿到最新更新后的dom,不過這里我要插一句話了,mouted和updated的執行,并不會等待所有子組件都被掛載完成后再執行,所以如果你希望所有視圖都更新完畢后再做些什么事情,那么你最好在mouted或者updated中加一個$nextTick(),然后把要做的事情放在$netTick()中去做(至于為什么,以后講到$nextTick再說吧)
- 再之后beforeDestroy沒啥說的,實體銷毀前,也就是說在這個函式內,你還是可以操作實體的
- 之后會做一系列的銷毀動作,解除各種資料參考,移除事件監聽,洗掉組件_watcher,洗掉子實體,洗掉自身self等,同時將實體屬性_isDestroyed置為true
- 銷毀完成后,再執行destroyed
示例
大致程序就是這樣,下面我們來通過例子來看一看
<body>
<div id="app">
<p>{{message}}</p>
<button @click="changeMsg">改變</button>
</div>
</body>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------')
console.log(this.message)
console.log(this.$el)
},
created () {
console.log('------初始化完成------')
console.log(this.message)
console.log(this.$el)
},
beforeMount () {
console.log('------掛載前---------')
console.log(this.message)
console.log(this.$el)
},
mounted () {
console.log('------掛載完成---------')
console.log(this.message)
console.log(this.$el)
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el)
}
})
</script>
我們先看看首次加載時,輸出了啥

從上面我們可以看出幾點,
- 首次,只執行了4個生命周期,beforeCreate,created, beforeMount, mounted,
- 同時,我們可以看出,第一個生命周期中,我們拿不到data中的資料,因為這個時候資料還未初始化
- created中,我們可以拿到data中的message資料了,因為初始化已經完成
- beforeMount中,我們可以看出,我們拿到了$el,而mounted中,我們也拿到了$el, 不過好像有點不一樣是吧,一個好像是渲染前的,一個是渲染后的,對的,看過MVVM回應式原來或者Vue原始碼你們就會發現,最初其實我們是會去讓this.$el = new Vue時傳入的那個el的dom,所以在beforMount中,其實我們拿到的就是頁面中的#app,而再繼續往后,首先我們是不是沒有找到render函式啊,也沒有找到template啊,所以他會怎么做啊,是不是會把我們的這個el(#app)編譯成template模板啊,再轉換為render函式,最后將render函式渲染成為真實dom,渲染成真實dom后,我們是不是會用這個渲染出來的dom去替換原來的vm.$el啊,這也就是我們前面所說到的替換$el是什么意思了,
- 所以, 在mounted中,我們所得到的渲染完成后的$el,
下面我們再看個例子
var vm = new Vue({
el: '#app',
data: {
message: 'hello world'
},
template: '<div>我是模板內的{{message}}</div>',
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------')
console.log(this.message)
console.log(this.$el)
},
created () {
console.log('------初始化完成------')
console.log(this.message)
console.log(this.$el)
},
beforeMount () {
console.log('------掛載前---------')
console.log(this.message)
console.log(this.$el)
},
mounted () {
console.log('------掛載完成---------')
console.log(this.message)
console.log(this.$el)
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el)
}
})
我們在new Vue實體的時候直接傳入了一個template,這時候我們再看輸出

這么看是不是就很清晰了啊 ,在beforeMount的時候,$el還是#app, 但是在mounted的時候就變成模板的div了,是不是因為我們傳了個template啊,所以,他直接將這個template轉換成render函式啦,再渲染成真實dom后,用渲染出來的真實dom替換了原來的$el,
下面我們洗掉上面的template, 點擊按鈕更改下message,查看輸出

哎,,,有沒有看到一個很奇怪的東西啊,在beforeUpdate中輸出的$el居然和updated里面輸出的是一樣的,這不對啊,以我們上面所說的邏輯的話,beforeUpdate內的$el應該是更新前的啊,這是怎么回事呢,這時候我們先來看一下mounted里面的,mounted里面我們看到p標簽內依舊是hello world 對不對,其實這是因為,我是先點擊了#app那個div的箭頭,將這個div展開了以后,我再點擊的按鈕去更改了message,所以mounted里面還是原來的,那我現在如果先不展開mounted里面的div的話,我們來看看會怎么樣

可以看到,初始輸出,其實是這樣的,我們看不到#app內的東西,需要點擊箭頭展開才能看到,現在,我不展開,然后我先點擊按鈕去改變message, 等beforUpdate和updated都執行完成后,我們再來一起展開,看下會怎么樣

這是點擊改變了message后的截圖,然后我們現在展開div看看

看到沒有,我們發現什么啦,怎么現在mounted里面的$el也變成更新后的啦,
呵呵,不要慌,其實啊,因為this.$el是一個物件,其實本質就是一個指標,當我們剛console.log輸出的時候,其實并沒有顯示內容,而當我們點擊箭頭去展開這個div的時候,將指標指向了當前的$el,所以我們看到的才會都是改變后的$el,這也就是為什么之前mounted里面的$el是改變之前的值,而現在是改變之后的值了,因為之前那張圖,我是先展開了mounted中的div,再去改變的message,下面我們再來驗證下是不是這么回事
怎么驗證,我們修改下代碼
mounted () {
console.log('------掛載完成---------')
console.log(this.message)
console.log(this.$el.innerHTML)
console.log(this.$el)
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el.innerHTML)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el.innerHTML)
console.log(this.$el)
}
我們增加一個輸出 this.$el.innerHTML, 再查看結果

這么看是不是就很明了啦,beforeUpdate里面的$el的內容,確實還是改變之前的,而我們之前看到的,只是因為我們后面展開時指標指向了當前值才導致的,是個視覺差而已,
后面兩個銷毀的,我就不舉例說明了,沒啥說的,下面我們再看一個問題,就是如果我們沒有設定el時,會怎么樣,我們在之前的生命周期圖中,是說過,當沒有找到el時, 說是不是會等待vm.$mount(el) 啊,這句話啥意思,我們來看一下

首先,我們看下,vue原始碼中,

在執行完,beforeCreate和created之后,是做了個判斷,當存在el時,呼叫了 $mount方法,created之后的步驟,就是在這里面去走的,那如果沒有el呢, 生命周期圖中是說等待vm. $mount呼叫,那是不是只能等待我們手動去呼叫啊,
var vm = new Vue({
data: {
message: 'hello world'
},
// template: '<div>我是模板內的{{message}} <button @click="changeMsg">點我</button></div>',
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------')
console.log(this.message)
console.log(this.$el)
},
created () {
console.log('------初始化完成------')
console.log(this.message)
console.log(this.$el)
},
beforeMount () {
console.log('------掛載前---------')
console.log(this.message)
console.log(this.$el)
},
mounted () {
console.log('------掛載完成---------')
console.log(this.message)
console.log(this.$el.innerHTML)
console.log(this.$el)
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el.innerHTML)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el.innerHTML)
console.log(this.$el)
}
})
這個時候,我們洗掉了el屬性,看看結果

是不是只走了前面兩個生命周期啊,后面就沒走了,這個時候其實就是在等$mount被呼叫了,那我們加個按鈕,點擊按鈕,手動呼叫一下$mount看會怎樣

沒點擊之前

點擊后

可以看到,生命周期繼續往下走了,
這時候不知道大家是不是想起來,看到有些vue專案的main.js里面是這樣的
export default new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
})
而有些vue專案中人家用的又是這樣的
export default new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount('#app')
其實后者,就相當于是手動呼叫了$mount了,
好了,言盡于此,有沒有看懂的朋友,請直接私信或者評論,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/240552.html
標籤:其他
