背景與總結
先説結論:
有很多所謂的最佳實踐告訴你這個觀點,但其實這個觀點是非常片面的,甚至在大部分場景下他是錯的。
聲明一下:
本文通過幾個案例説明部分同行觀點的片面性,分析了這種觀點出現原因,涉及一點vue的渲染原理。最後給出作為個人為了避免困擾,可以採取的相對最近實踐(因人而異)。
分析
很多文章給出的理由是:mounted回調函數被調用的時候,組件已經被掛載到了DOM上,而比mounted更早的生命週期還沒有掛載。
這句話本身沒有毛病,但它和mounted裏執行異步請求沒有必然的因果關係。
關鍵就在於,mounted回調執行時,組件已經掛載到了DOM上,可我們的異步請求與DOM有什麼關係嗎?
並沒有直接的關係,雖然我們請求的結果需要通過DOM呈現在界面上,用户才能看到。
但是,別忘了,通常我們的請求結果是經過處理然後複製給data的一個屬性,就像這樣:
getData(){
axios.get(url).then(response => {
// 這裏只是一個複製操作
this.dataset = response.data
})
}
我們只是做了一個賦值操作,並沒有操作DOM。
之後這個賦值操作會被vue攔截到,然後去調用render函數生成新的vnode,進一步去做diff找出需要更新的屬性、節點、樣式等,最後會有真正的讀寫DOM的操作。
這也正是為什麼都説要放在mounted回調中執行異步請求的原因,因為賦值操作會導致DOM的操作。
但這個理由根本不成立,關鍵就在於異步這倆字。
如果你要做一個同步的請求,那麼我同意上述的觀點,但不好意思,你不是同步的。
案例
來看兩個例子對比下:
第一個,我們在created中有個異步操作,修改了requested屬性。
<template>
<h1>{{ requested }}</h1>
</template>
<script>
export default {
data() {
return {
requested: '',
}
},
created() {
console.log('created')
setTimeout(() => {
this.requested = 'done!'
console.log('setTimeout!')
})
},
mounted() {
console.log('mounted')
},
}
</script>
結果:代碼正常運行,界面正常展示,並沒有發生你所擔心的created中讀寫DOM出錯的問題。而且從輸出的日誌可以看出來,異步回調是在mounted之後才執行的,這個很關鍵。
如果換成同步操作呢?
<template>
<h1>{{ requested }}</h1>
</template>
<script>
export default {
data() {
return {
requested: '',
}
},
created() {
console.log('created')
this.requested = 'requested finished!'
console.log('requested finished!')
},
mounted() {
console.log('mounted')
},
}
</script>
結果:代碼正常運行,界面正常展示,也沒有發生你所擔心的created中讀寫DOM出錯的問題。
而從輸出的日誌可以看出來,created中的同步操作是在mounted之前執行的。
這裏很奇怪,儘管我們修改了data中的一個狀態會導致vue去讀寫DOM,但此時並沒有發生錯誤。
Vue渲染原理(部分)
實際上,這裏需要簡單瞭解下vue的渲染流程-diff,diff會對oldVnode和newVnode進行對比,在開始的時候有個很簡單的邏輯,以下是偽代碼:
if (!oldVnode){
// 沒有上一次的 vnode,説明是首次渲染
// 直接執行掛載方法,不需要再進行其它的比較
mountComponent()
} else {
// 正常的屬性、子節點等的對比
}
所以,created中同步的修改狀態不用擔心讀寫DOM出錯的問題。
異步請求放在哪裏
業內同行給出mounted中執行異步代碼的建議是相對安全的一種選擇,對於部分初學者來説,避免了業務開發的難度,避免了疑難問題的出現頻率。
由於前端各種開發人員對vue的原理認識不足,憑着自身的經驗給出這個最佳實踐是可以理解的。
實際上,通過上面的案例可以看到,將異步/同步請求放在created裏執行是可以的,當然有個前提哈:請求的回調中不能直接讀寫DOM。
甚至,你異步(同步不行)請求放在beforeCreate中執行都是可行的,請求的回調中不能直接讀寫DOM依然是前提。
<template>
<h1>{{ requested }}</h1>
</template>
<script>
export default {
data() {
return {
requested: '',
}
},
beforeCreate() {
console.log('beforeCreate')
setTimeout(() => {
this.requested = 'done!'
console.log('setTimeout!')
})
},
created() {
console.log('created')
},
mounted() {
console.log('mounted')
},
}
</script>
結果:
代碼執行運行,界面正常展示,很匪夷所思吧。
哈哈,有個重要的點,vue中的渲染流程init、beforeCreate、created、beforeMount、mounted等生命週期鈎子的調用是同步的,所以異步的請求回調都會在這些同步鈎子執行完後才可能執行(可能的意思是,還得等其它同步任務執行完成)。
這裏的原理涉及到js的事件循環,不過多展開了,有興趣可以看看這篇文章【JS】深入理解事件循環,這一篇就夠了!。
另外,以上分析案例都是基於vue2.x的版本,實際上,使用vue3.x,結論依然成立。
綜上所述
- 如果你是初學者或者只是臨時使用
vue實現簡單的業務,不想過多的深入,那麼直接在mounted中執行異步代碼一般沒有明顯的問題; - 如果你是追求更好的性能,以及更深入的理解
vue,那麼不妨更深入的思考下最佳實踐原因,它是否適合你以及你的項目呢。