react高階面試題中有這麼一道:為什麼非同步請求資料在didMount階段更合適?同為MVVM中的翹楚,Vue是否也有類似問題呢?另外,我在平時也無開發過程中也會發現,每個人選擇的那個生命週期階段去非同步請求資料總會不一樣,因此引發思考,到底哪個階段更適合非同步請求資料呢?在產品設計和使用者體驗方面又會有哪些影響?本篇記錄就是為了解決這兩個問題。
一、Vue生命週期
首先再老話重提過一下Vue生命週期,以及每個階段都做了什麼事。
1. beforeCreated:生成$options選項,並給例項新增生命週期相關屬性。在例項初始化之後,在 資料觀測(data observer) 和event/watcher 事件配置之前被呼叫,也就是說,data,watcher,methods都不存在這個階段。但是有一個物件存在,那就是$route,因此此階段就可以根據路由資訊進行重定向等操作。
2. created:初始化與依賴注入相關的操作,會遍歷傳入methods的選項,初始化選項資料,從$options獲取資料選項(vm.$options.data),給資料新增‘觀察器’物件並建立觀察器,定義getter、setter儲存器屬性。在例項建立之後被呼叫,該階段可以訪問data,使用watcher、events、methods,也就是說 資料觀測(data observer) 和event/watcher 事件配置 已完成。但是此時dom還沒有被掛載。該階段允許執行http請求操作。
3. beforeMount:將HTML解析生成AST節點,再根據AST節點動態生成渲染函式。相關render函式首次被呼叫(劃重點)。
4. mounted:在掛載完成之後被呼叫,執行render函式生成虛擬dom,建立真實dom替換虛擬dom,並掛載到例項。可以操作dom,比如事件監聽
5. beforeUpdate:$vm.data更新之後,虛擬dom重新渲染之前被呼叫。在這個鉤子可以修改$vm.data,並不會觸發附加的衝渲染過程。
6. updated:虛擬dom重新渲染後呼叫,若再次修改$vm.data,會再次觸發beforeUpdate、updated,進入死迴圈。
7. beforeDestroy:例項被銷燬前呼叫,也就是說在這個階段還是可以呼叫例項的。
8. destroyed:例項被銷燬後呼叫,所有的事件監聽器已被移除,子例項被銷燬。
總結來說,虛擬dom開始渲染是在beforeMount時,dom例項掛載完成在mounted階段顯示。
那麼接下來了解就是render函式。
render示例:export default {
data () {
return {
menu_items: [] // 請求返回如:[{fullname: '頁面一'},{fullname: '頁面二'},{fullname: '頁面三'},{fullname: '頁面四'}]
}
}, render (createElement){ return createElement(
// 1. 第一個引數,要渲染的標籤名稱(必填)
'ul',
// 2. 第二個引數,1中要渲染的標籤的屬性,或者文字元素(可選) {
class: {'uk-nav': true}, }, // 3. 第三個引數,1中標籤的子元素,詳情看官方文件(可選)
this.menu_items.map(item=>createElement('li',item.fullname)))
) }}複製程式碼
render函式最終返回的是createNodeDescription(節點描述),即俗稱virtual node(虛擬節點)。用template寫的話,就是下面這樣:
<template>
<ul> <li v-for="item in menu_items"> {{ item.fullname }} </li> </ul>
</template>複製程式碼
這個過程在mounted被呼叫前完成。詳細參考可移步 這裡
二、非同步載入
setTimeout等非同步函式
非同步函式跟同步函式的不同之處,最大的應該就是非同步函式會等到所有同步函式執行完成之後再執行。具體的可以看 事件迴圈 。
//data欄位有個num
created: function () {
console.group('created 建立完畢狀態===============》')
console.log('%c%s', 'color:red', 'el : ' + this.$el) // undefined
console.log('%c%s', 'color:red', 'data : ' + this.$data) // 已被初始化
console.log('%c%s', 'color:red', 'message: ' + this.message) // 已被初始化
//新增程式碼片段
setTimeout(() => { //這裡只是為了偷懶用了ES6的箭頭函式,如果是普通函式請注意this指標修改,vue中請不要濫用箭頭函式,出了問題找都找不到
this.num ++
this.num += 2
}, 0) //注意這裡的延時都是0
setTimeout(() => {
this.num -= 5
}, 0)
}複製程式碼
控制檯答應結果:
(略失真。。。截圖太大稍微壓縮了下!!--)
vue在執行程式碼的時候,並沒有去管定時器裡發生了什麼事情,甚至已經設定了0延時,他依舊會去順序執行其他生命週期,看起來就像跳過了這些非同步載入。因此可以確定一點,生命週期中的非同步操作不會按照順序執行,而是會等到非非同步操作結束後執行。因此書寫這部分程式碼的時候請注意裡面的邏輯不要和順序掛鉤,要確保任何非同步操作即使最後執行,之前的程式也不會發生異常從而阻塞整個程式。
ajax非同步請求
ajax請求是非同步操作,回撥函式的執行時間是不確定的。也就是說,即使在created鉤子傳送請求,dom被掛載之後請求仍沒有返回結果,就很有可能導致執行出錯,諸如:
因為此時上述render事例中的menu_items還是空置。
解決方案
針對ajax非同步請求,這樣的錯誤原因其實就是因為返回結果沒趕上dom節點的渲染。所以可以從兩方面做修改:一是返回結果的賦值變數上,另一個就是dom節點的渲染層面。
1. 給予賦值變數初始值,即定義時menu_items:[ {fullname: ''} ]。
這麼做的好處就是頁面節點的渲染不受限於返回結果,靜態文案照樣會被渲染,動態資料則會在資料更新時被填充。給使用者的感覺就是,頁面渲染速度不錯。
但是這種方式也有缺陷,後臺返回資料欄位不盡相同,要是都這麼寫那就真是麻煩了。
當然如果你使用typescript就沒有這種煩惱,menu_items: { [propName: string]: any } = {}就搞定了。
2. v-if,控制dom節點的掛載,當且僅當menu_items被賦予返回值時,才開始渲染節點。
這麼做的好處就是靜態和動態文案同步展現在使用者面前,不會有文案跳動,資料從無到有的過程。但是,副作用就是頁面渲染時間、使用者等待時間變長。
那如果dom掛載前請求資料已經返回了,又會是怎樣的結果呢?
我們可以用setTimeout來模擬一下這個過程
<span>{{person.name.firstName}}</span>
data: function () {
return {
message: 'hello world',
add: 1,
person: {
name: {}
}
}
},
created: function () {
console.group('created 建立完畢狀態===============》')
console.log('%c%s', 'color:red', 'el : ' + this.$el) // undefined
console.log('%c%s', 'color:red', 'data : ' + this.$data) // 已被初始化
console.log('%c%s', 'color:red', 'message: ' + this.message) // 已被初始化
//假裝介面返回了一些資訊給你,如一個人,然後你把這些資訊賦值給了this實力
setTimeout(() => {
this.person = {
name: {
lastName: 'carry',
firstName: 'dong'
},
sex: '男'
}
}, 0)複製程式碼
請求夠早了吧,但還是報錯了,this.person.name.firstName 是 undefined,不過程式報完錯後還是再繼續執行了。
三、結論
既然非同步函式並不會阻塞vue生命週期整個程式,那麼在哪個階段請求都可以。如果考慮到使用者體驗方面的影響,希望使用者今早感知頁面已載入,減少空白頁面時間,建議就放在created階段了,然後再處理會出現null、undefined這種情況就好。畢竟越早獲取資料,在mounted例項掛載的時候渲染也就越及時。
當然即使是這種情況下,也不排除會觸發updated生命鉤子(data有預設值且已渲染,之後資料被更新),從而導致虛擬dom的重新渲染。