Vue原始碼解讀系列篇
- 1. Vue響應式原理-理解Observer、Dep、Watcher
- 2. Vue響應式原理-如何監聽Array的變化
- 3. Vue響應式原理-如何監聽Array的變化?詳細版
- 4. Vue非同步更新 - nextTick為什麼要microtask優先?
一、Vue非同步更新佇列
(1)Vue非同步更新
相信大家都知道,Vue
可以做到資料驅動檢視更新,比如我們就簡單寫一個事件如下:
methods: {
tap() {
for (let i = 0; i < 10; i++) {
this.a = i;
}
this.b = 666;
},
},
複製程式碼
當我們觸發這個事件,檢視中的a
和b
肯定會發現一些變化。
那我們思考一下,Vue
是如何管理這個變化的過程?比如上面這個案例,a
被迴圈了10次,那Vue
會去渲染檢視10次嗎?顯然不會,畢竟這個效能代價非常大。畢竟我們只需要a
最後一次的賦值。
實際上Vue
是非同步更新檢視的,也就是說會等這個tap
事件執行完,檢查發現只需要更新a
和b
,然後再一次性更新,避免無效的更新。
Vue
官方文件也印證了我們的想法,如下:
Vue 在更新 DOM 時是非同步執行的。只要偵聽到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料變更。如果同一個 watcher 被多次觸發,只會被推入到佇列中一次。這種在緩衝時去除重複資料對於避免不必要的計算和 DOM 操作是非常重要的。
以上可以詳見Vue官方文件 - 非同步更新佇列。
(2)派發更新中的非同步佇列
Vue
通知檢視更新,是通過dep.notify
,相信你讀到這裡肯定是瞭解Vue
響應式原理的。那麼來檢視下dep.notify
都做了什麼?耐心點,離真相越來越近了。
// dep.js
notify () {
const subs = this.subs.slice();
// 迴圈通知所有watcher更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
複製程式碼
首先迴圈通知所有watcher
更新,我們發現watcher
執行了update
方法。
// watcher.js
update () {
if (this.lazy) {
// 如果是計算屬性
this.dirty = true
} else if (this.sync) {
// 如果要同步更新
this.run()
} else {
// 進入更新佇列
queueWatcher(this)
}
}
複製程式碼
update
方法首先判斷是不是計算屬性或開發者定義了同步更新,這些我們先不看,直接進入正題,進入非同步佇列方法queueWatcher
。
那麼再來看下queueWatcher
,我省略了絕大部分程式碼,畢竟程式碼是枯燥的,為了方便大家理解,都是一些思路性程式碼。
export function queueWatcher (watcher: Watcher) {
// 獲取watcherid
const id = watcher.id
if (has[id] == null) {
// 保證只有一個watcher,避免重複
has[id] = true
// 推入等待執行的佇列
queue.push(watcher)
// ...省略細節程式碼
}
// 將所有更新動作放入nextTick中,推入到非同步佇列
nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue () {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
// ...省略細節程式碼
}
}
複製程式碼
通過上述程式碼可以看出我們將所有要更新的watcher
佇列放入了nextTick
中。
nextTick
的官方解讀為:
在下次 DOM 更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的 DOM。
這裡的描述其實限制了nextTick
的技能,實際上nextTick
就是一個非同步方法,也許和你使用的setTimeout
沒有太大的區別。
那來看下nextTick
的原始碼究竟做了什麼?
二、nextTick原始碼淺析
nextTick
原始碼很少,翻來翻去沒幾行,但是我也不打算展開講,因為看程式碼真的很枯燥。
下面的程式碼只有幾行,其實你可以選擇跳過看結論。
// timerFunc就是nextTick傳進來的回撥等... 細節不展開
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
// 當原生 Promise 不可用時,timerFunc 使用原生 MutationObserver
// MutationObserver不要在意它的功能,其實就是個可以達到微任務效果的備胎
)) {
timerFunc = () => {
// 使用 MutationObserver
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 如果原生 setImmediate 可用,timerFunc 使用原生 setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最後的倔強,timerFunc 使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
複製程式碼
總結就是Promise
> MutationObserver
> setImmediate
> setTimeout
。
果然和setTimeout
沒有太大的區別~
再總結一下優先順序:microtask (jobs)
優先。
nextTick
原始碼為什麼要microtask
優先?再理解這個問題答案之前,我們還要複習eventLoop
知識。
三、eventLoop
(1)任務佇列
用2張圖帶大家簡單回憶一下,但是就不細講了,大家可以自行查詢資料。
- 我們的同步任務在主執行緒上執行會形成一個執行棧。
- 如果碰到非同步任務,比如
setTimeout
、onClick
等等的一些操作,我們會將他的執行結果放入佇列,此期間主執行緒不阻塞。 - 等到主執行緒中的所有同步任務執行完畢,就會通過
event loop
在佇列裡面從頭開始取,在執行棧中執行event loop
永遠不會斷。 - 以上的這一整個流程就是
Event Loop
(事件迴圈機制)。
(2)微任務、巨集任務
- 每次執行棧的同步任務執行完畢,就會去任務佇列中取出完成的非同步任務,但是佇列中又分為微任務
microtask
和巨集任務tasks
佇列 - 等到把所有的微任務
microtask
都執行完畢,注意是所有的,他才會從巨集任務tasks
佇列中取事件。 - 等到把佇列中的事件取出一個,放入執行棧執行完成,就算一次迴圈結束。
- 之後
event loop
還會繼續迴圈,他會再去微任務microtask
執行所有的任務,然後再從巨集任務tasks
佇列裡面取一個,如此反覆迴圈。
四、nextTick為什麼要儘可能的microtask優先?
簡單的回憶了eventLoop
、微任務、巨集任務後,我們還要再丟擲一個結論。
我們發現,原來在執行微任務之後還會執行渲染操作!!!(當然並不是每次都會,但至少順序我們是可以肯定的)。
- 在一輪
event loop
中多次修改同一dom
,只有最後一次會進行繪製。 - 渲染更新(
Update the rendering
)會在event loop
中的tasks
和microtasks
完成後進行,但並不是每輪event loop
都會更新渲染,這取決於是否修改了dom
和瀏覽器覺得是否有必要在此時立即將新狀態呈現給使用者。如果在一幀的時間內(時間並不確定,因為瀏覽器每秒的幀數總在波動,16.7ms只是估算並不準確)修改了多處dom
,瀏覽器可能將變動積攢起來,只進行一次繪製,這是合理的。 - 如果希望在每輪
event loop
都即時呈現變動,可以使用requestAnimationFrame
。
這裡我丟擲結論,原因和理論知識可以看這篇文章 從event loop規範探究javaScript非同步及瀏覽器更新渲染時機 ,這位大神寫的很好。
不知道大家有沒有猜出【nextTick
為什麼要儘可能的microtask
優先?】
這裡又盜了大神的圖,event loop
的大致迴圈過程:
假設現在執行到某個 task,我們對批量的dom
進行非同步修改,我們將此任務插進tasks
,也就是用巨集任務實現。
顯而易見,這種情況下如果task
裡排隊的佇列比較多,同時遇到多次的微任務佇列執行完。那很有可能觸發多次瀏覽器渲染,但是依舊沒有執行我們真正的修改dom
任務。
這種情況,不僅會延遲檢視更新,帶來效能問題。還有可能導致檢視上一些詭異的問題。
因此,此任務插進microtasks
:
task
佇列如果有大量的任務等待執行時,將dom
的變動作為microtasks
而不是巨集任務(task
)能更快的將變化呈現給使用者。
總結
之所以講這篇文章,是因為在最近在讀Vue的原始碼,我看的是2.6.10, 發現nextTick和2.5版本的實現不太一樣。大家可以看下這位大佬的文章 Vue.js 升級踩坑小記
文章內容基本都是在其他大佬的基礎上進行的理解,講錯的大家可以批評指正~
參考文章
文章有一些結論直接參考其他文章,自己實在是懶得寫啦~~
侵權刪 ^^