本文主要介紹了從Vue.js源碼看異步更新DOM策略及nextTick,具有一定的參考價值,有興趣的小伙伴們可以參考一下,希望能幫助大家更好地理解Vue.js異步。
寫在前面
因為對Vue.js很感興趣,而且平常工作的技術堆疊也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js源碼,並做了總結與輸出。
文章的原始網址:https://github.com/answershuto/learnVue。
在學習過程中,為Vue加上了中文的註解https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以對其他想學習Vue原始碼的小夥伴有幫助。
可能會有理解有偏差的地方,歡迎提issue指出,共同學習,共同進步。
操作DOM
在使用vue.js的時候,有時候因為一些特定的業務場景,不得不去操作DOM,例如這樣:
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
列印的結果是begin,為什麼我們明明已經將test設定成了“end”,獲取真實DOM節點的innerText卻沒有得到我們預期中的“end”,而是得到先前的值“begin”呢?
Watcher佇列
帶著疑問,我們找到了Vue.js原始碼的Watch實作。當某個響應式資料改變的時候,它的setter函數會通知閉包中的Dep,Dep則會呼叫它所管理的所有Watch物件。觸發Watch物件的update實作。我們來看看update的實作。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
我們發現Vue.js預設是使用非同步執行DOM更新。
當非同步執行update的時候,就會呼叫queueWatcher函數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
查看queueWatcher的原始碼我們發現,Watch物件並不是立即更新視圖,而是被push進了一個佇列queue,此時狀態處於waiting的狀態,這時候會繼續會有Watch物件被push進這個佇列queue,等待下一個tick時,這些Watch物件才會被遍歷取出,更新視圖。同時,id重複的Watcher不會被多次加入到queue中去,因為在最終渲染時,我們只需要關心資料的最終結果。
那麼,什麼是下一個tick?
nextTick
vue.js提供了一個nextTick函數,其實也就是上面所呼叫的nextTick。
nextTick的實作比較簡單,執行的目的是在microtask或task中推入一個funtion,在目前堆疊執行完畢(也行還會有一些排在前面的需要執行的任務)以後執行nextTick傳入的funtion,看一下原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
|
它是一個立即執行函數,傳回一個queueNextTick介面。
傳入的cb會被push進callbacks中存放起來,然後執行timerFunc(pending是狀態標記,保證timerFunc在下一個tick之前只執行一次)。
timerFunc是什麼?
看了源碼發現timerFunc會檢測當前環境而不同實現,其實就是按照Promise,MutationObserver,setTimeout優先級,哪個存在使用哪個,最不濟的環境下使用setTimeout。
這裡解釋一下,總共有Promise、MutationObserver以及setTimeout三種嘗試得到timerFunc的方法。
優先使用Promise,在Promise不存在的情況下使用MutationObserver,這兩個方法的回呼函數都會在microtask中執行,它們會比setTimeout更早執行,所以優先使用。
如果上述兩種方法都不支援的環境則會使用setTimeout,在task尾部推入這個函數,等待呼叫執行。
為什麼要優先使用microtask?我在顧軼事在知乎的回答中學習到:
JS 的event loop 執行時會區分task 和microtask,引擎在每個task 執行完畢,從隊列中取下一個task 來執行之前,會先執行完所有microtask 佇列中的microtask。
setTimeout 回呼會被指派到一個新的 task 中執行,而 Promise 的 resolver、MutationObserver 的回呼都會被安排到一個新的 microtask 中執行,而會比 setTimeout 產生的 task 先執行。
要建立一個新的 microtask,優先使用 Promise,如果瀏覽器不支持,再嘗試 MutationObserver。
實在不行,只能用 setTimeout 建立 task 了。
為啥要用 microtask?
根據 HTML Standard,在每個 task 運行完以後,UI 都會重渲染,那麼在 microtask 中就完成資料更新,目前 task 結束就可以得到最新的 UI 了。
反之如果新建一個 task 來做資料更新,那麼渲染就會進行兩次。
參考顧軼事靈知乎的回答
首先是Promise,(Promise.resolve()).then()可以在microtask中加入它的回調,
MutationObserver新建一個textNode的DOM對象,用MutationObserver綁定該DOM並指定回呼函數,在DOM變化的時候則會觸發回調,該回調會進入microtask,即textNode.data = String(counter)時便會加入該回調。
setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。
综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。
flushSchedulerQueue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
|
flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,用来更新视图
为什么要异步更新视图
来看一下下面这一段代码
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
现在有这样的一种情况,created的时候test的值会被++循环执行1000次。
每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。
如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。
所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。
保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。
访问真实DOM节点更新后的数据
所以我们需要在修改data中的数据后访问真实的DOM节点更新后的数据,只需要这样,我们把文章第一个例子进行修改。
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
使用Vue.js的global API的$nextTick方法,即可在回调中获取已经更新好的DOM实例了。
相关推荐:
以上是Vue.js非同步更新DOM策略及nextTick實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!