您好,登錄后才能下訂單哦!
這篇文章主要介紹“Vue異步更新機制和nextTick原理實例分析”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Vue異步更新機制和nextTick原理實例分析”文章能幫助大家解決問題。
update
方法的實現:
// src/core/observer/watcher.js /* Subscriber接口,當依賴發生改變的時候進行回調 */ update() { if (this.computed) { // 一個computed watcher有兩種模式:activated lazy(默認) // 只有當它被至少一個訂閱者依賴時才置activated,這通常是另一個計算屬性或組件的render function if (this.dep.subs.length === 0) { // 如果沒人訂閱這個計算屬性的變化 // lazy時,我們希望它只在必要時執行計算,所以我們只是簡單地將觀察者標記為dirty // 當計算屬性被訪問時,實際的計算在this.evaluate()中執行 this.dirty = true } else { // activated模式下,我們希望主動執行計算,但只有當值確實發生變化時才通知我們的訂閱者 this.getAndInvoke(() => { this.dep.notify() // 通知渲染watcher重新渲染,通知依賴自己的所有watcher執行update }) } } else if (this.sync) { // 同步 this.run() } else { queueWatcher(this) // 異步推送到調度者觀察者隊列中,下一個tick時調用 } }
如果不是 computed watcher
也非 sync
會把調用 update 的當前 watcher 推送到調度者隊列中,下一個 tick 時調用,看看 queueWatcher
:
// src/core/observer/scheduler.js /* 將一個觀察者對象push進觀察者隊列,在隊列中已經存在相同的id則 * 該watcher將被跳過,除非它是在隊列正被flush時推送 */ export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { // 檢驗id是否存在,已經存在則直接跳過,不存在則標記哈希表has,用于下次檢驗 has[id] = true queue.push(watcher) // 如果沒有正在flush,直接push到隊列中 if (!waiting) { // 標記是否已傳給nextTick waiting = true nextTick(flushSchedulerQueue) } } } /* 重置調度者狀態 */ function resetSchedulerState () { queue.length = 0 has = {} waiting = false }
這里使用了一個 has
的哈希map用來檢查是否當前 watcher 的 id 是否存在,若已存在則跳過,不存在則就 push 到 queue
隊列中并標記哈希表 has,用于下次檢驗,防止重復添加。這就是一個去重的過程,比每次查重都要去 queue 中找要文明,在渲染的時候就不會重復patch
相同 watcher 的變化,這樣就算同步修改了一百次視圖中用到的 data,異步 patch
的時候也只會更新最后一次修改。
這里的 waiting
方法是用來標記 flushSchedulerQueue
是否已經傳遞給 nextTick
的標記位,如果已經傳遞則只 push 到隊列中不傳遞 flushSchedulerQueue
給 nextTick
,等到 resetSchedulerState
重置調度者狀態的時候 waiting
會被置回 false
允許 flushSchedulerQueue
被傳遞給下一個 tick 的回調,總之保證了 flushSchedulerQueue
回調在一個 tick 內只允許被傳入一次。來看看被傳遞給 nextTick
的回調 flushSchedulerQueue
做了什么:
// src/core/observer/scheduler.js /* nextTick的回調函數,在下一個tick時flush掉兩個隊列同時運行watchers */ function flushSchedulerQueue () { flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) // 排序 for (index = 0; index < queue.length; index++) { // 不要將length進行緩存 watcher = queue[index] if (watcher.before) { // 如果watcher有before則執行 watcher.before() } id = watcher.id has[id] = null // 將has的標記刪除 watcher.run() // 執行watcher if (process.env.NODE_ENV !== 'production' && has[id] != null) { // 在dev環境下檢查是否進入死循環 circular[id] = (circular[id] || 0) + 1 // 比如user watcher訂閱自己的情況 if (circular[id] > MAX_UPDATE_COUNT) { // 持續執行了一百次watch代表可能存在死循環 warn() // 進入死循環的警告 break } } } resetSchedulerState() // 重置調度者狀態 callActivatedHooks() // 使子組件狀態都置成active同時調用activated鉤子 callUpdatedHooks() // 調用updated鉤子 }
在 nextTick
方法中執行 flushSchedulerQueue
方法,這個方法挨個執行 queue
中的watcher的 run
方法。我們看到在首先有個 queue.sort()
方法把隊列中的 watcher 按 id 從小到大排了個序,這樣做可以保證:
組件更新的順序是從父組件到子組件的順序,因為父組件總是比子組件先創建。
一個組件的 user watchers (偵聽器watcher)比 render watcher 先運行,因為 user watchers 往往比 render watcher 更早創建
如果一個組件在父組件 watcher 運行期間被銷毀,它的 watcher 執行將被跳過
在挨個執行隊列中的 for 循環中,index < queue.length
這里沒有將 length 進行緩存,因為在執行處理現有 watcher 對象期間,更多的 watcher 對象可能會被 push 進 queue。
那么數據的修改從 model 層反映到 view 的過程:數據更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新視圖
這里就來看看包含著每個 watcher 執行的方法被作為回調傳入 nextTick
之后,nextTick
對這個方法做了什么。不過首先要了解一下瀏覽器中的 EventLoop
、macro task
、micro task
幾個概念
解釋一下,當主線程執行完同步任務后:
引擎首先從 macrotask queue 中取出第一個任務,執行完畢后,將 microtask queue 中的所有任務取出,按順序全部執行;
然后再從 macrotask queue 中取下一個,執行完畢后,再次將 microtask queue 中的全部取出;
循環往復,直到兩個 queue 中的任務都取完。
瀏覽器環境中常見的異步任務種類,按照優先級:
macro task
:同步代碼、setImmediate
、MessageChannel
、setTimeout/setInterval
micro task
:Promise.then
、MutationObserver
有的文章把 micro task
叫微任務,macro task
叫宏任務,因為這兩個單詞拼寫太像了 -。- ,所以后面的注釋多用中文表示~
先來看看源碼中對 micro task
與 macro task
的實現:macroTimerFunc
、microTimerFunc
// src/core/util/next-tick.js const callbacks = [] // 存放異步執行的回調 let pending = false // 一個標記位,如果已經有timerFunc被推送到任務隊列中去則不需要重復推送 /* 挨個同步執行callbacks中回調 */ function flushCallbacks() { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } let microTimerFunc // 微任務執行方法 let macroTimerFunc // 宏任務執行方法 let useMacroTask = false // 是否強制為宏任務,默認使用微任務 // 宏任務 if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || MessageChannel.toString() === '[object MessageChannelConstructor]' // PhantomJS )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } } // 微任務 if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { p.then(flushCallbacks) } } else { microTimerFunc = macroTimerFunc // fallback to macro }
flushCallbacks
這個方法就是挨個同步的去執行 callbacks 中的回調函數們, callbacks 中的回調函數是在調用 nextTick
的時候添加進去的;那么怎么去使用 micro task
與 macro task
去執行 flushCallbacks
呢,這里他們的實現 macroTimerFunc
、microTimerFunc
使用瀏覽器中宏任務/微任務的 API 對flushCallbacks
方法進行了一層包裝。比如宏任務方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) }
,這樣在觸發宏任務執行的時候 macroTimerFunc()
就可以在瀏覽器中的下一個宏任務 loop 的時候消費這些保存在 callbacks 數組中的回調了,微任務同理。同時也可以看出傳給 nextTick
的異步回調函數是被壓成了一個同步任務在一個 tick 執行完的,而不是開啟多個異步任務。
注意這里有個比較難理解的地方,第一次調用 nextTick
的時候 pending
為 false ,此時已經 push 到瀏覽器 event loop 中一個宏任務或微任務的 task,如果在沒有 flush 掉的情況下繼續往 callbacks 里面添加,那么在執行這個占位 queue 的時候會執行之后添加的回調,所以 macroTimerFunc
、microTimerFunc
相當于 task queue 的占位,以后 pending
為 true 則繼續往占位 queue 里面添加,event loop 輪到這個 task queue 的時候將一并執行。執行 flushCallbacks
時 pending
置 false,允許下一輪執行 nextTick
時往 event loop 占位。
可以看到上面 macroTimerFunc
與 microTimerFunc
進行了在不同瀏覽器兼容性下的平穩退化,或者說降級策略:
macroTimerFunc
:setImmediate -> MessageChannel -> setTimeout
。首先檢測是否原生支持 setImmediate
,這個方法只在 IE、Edge 瀏覽器中原生實現,然后檢測是否支持 MessageChannel,如果對 MessageChannel
不了解可以參考一下這篇文章,還不支持的話最后使用 setTimeout
;為什么優先使用 setImmediate
與 MessageChannel
而不直接使用 setTimeout
呢,是因為 HTML5 規定 setTimeout 執行的最小延時為4ms,而嵌套的 timeout 表現為10ms,為了盡可能快的讓回調執行,沒有最小延時限制的前兩者顯然要優于 setTimeout
。
microTimerFunc
:Promise.then -> macroTimerFunc
。首先檢查是否支持Promise
,如果支持的話通過 Promise.then
來調用 flushCallbacks
方法,否則退化為 macroTimerFunc
;vue2.5之后 nextTick
中因為兼容性原因刪除了微任務平穩退化的 MutationObserver
的方式。
最后來看看我們平常用到的 nextTick
方法到底是如何實現的:
// src/core/util/next-tick.js export function nextTick(cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } /* 強制使用macrotask的方法 */ export function withMacroTask(fn: Function): Function { return fn._withTask || (fn._withTask = function() { useMacroTask = true const res = fn.apply(null, arguments) useMacroTask = false return res }) }
nextTick
在這里分為三個部分,我們一起來看一下;
首先 nextTick
把傳入的 cb
回調函數用 try-catch
包裹后放在一個匿名函數中推入callbacks數組中,這么做是因為防止單個 cb
如果執行錯誤不至于讓整個JS線程掛掉,每個 cb
都包裹是防止這些回調函數如果執行錯誤不會相互影響,比如前一個拋錯了后一個仍然可以執行。
然后檢查 pending
狀態,這個跟之前介紹的 queueWatcher
中的 waiting
是一個意思,它是一個標記位,一開始是 false
在進入 macroTimerFunc
、microTimerFunc
方法前被置為 true
,因此下次調用 nextTick
就不會進入 macroTimerFunc
、microTimerFunc
方法,這兩個方法中會在下一個 macro/micro tick
時候flushCallbacks
異步的去執行callbacks隊列中收集的任務,而 flushCallbacks
方法在執行一開始會把 pending
置 false
,因此下一次調用 nextTick
時候又能開啟新一輪的 macroTimerFunc
、microTimerFunc
,這樣就形成了vue中的 event loop
。
最后檢查是否傳入了 cb
,因為 nextTick
還支持Promise化的調用:nextTick().then(() => {})
,所以如果沒有傳入 cb
就直接return了一個Promise實例,并且把resolve傳遞給_resolve,這樣后者執行的時候就跳到我們調用的時候傳遞進 then
的方法中。
Vue源碼中 next-tick.js
文件還有一段重要的注釋,這里就翻譯一下:
在vue2.5之前的版本中,nextTick基本上基于
micro task
來實現的,但是在某些情況下micro task
具有太高的優先級,并且可能在連續順序事件之間(例如#4521,#6690)或者甚至在同一事件的事件冒泡過程中之間觸發(#6566)。但是如果全部都改成macro task
,對一些有重繪和動畫的場景也會有性能影響,如 issue #6813。vue2.5之后版本提供的解決辦法是默認使用micro task
,但在需要時(例如在v-on附加的事件處理程序中)強制使用macro task
。
為什么默認優先使用 micro task
呢,是利用其高優先級的特性,保證隊列中的微任務在一次循環全部執行完畢。
強制 macro task
的方法是在綁定 DOM 事件的時候,默認會給回調的 handler 函數調用withMacroTask
方法做一層包裝 handler = withMacroTask(handler)
,它保證整個回調函數執行過程中,遇到數據狀態的改變,這些改變都會被推到 macro task
中。以上實現在 src/platforms/web/runtime/modules/events.js 的 add
方法中,可以自己看一看具體代碼。
說這么多,不如來個例子,執行參見 CodePen
<div id="app"> <span id='name' ref='name'>{{ name }}</span> <button @click='change'>change name</button> <div id='content'></div> </div> <script> new Vue({ el: '#app', data() { return { name: 'SHERlocked93' } }, methods: { change() { const $name = this.$refs.name this.$nextTick(() => console.log('setter前:' + $name.innerHTML)) this.name = ' name改嘍 ' console.log('同步方式:' + this.$refs.name.innerHTML) setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML)) this.$nextTick(() => console.log('setter后:' + $name.innerHTML)) this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML)) } } }) </script>
執行以下看看結果:
同步方式:SHERlocked93 setter前:SHERlocked93 setter后:name改嘍 Promise方式:name改嘍 setTimeout方式:name改嘍
為什么是這樣的結果呢,解釋一下:
同步方式: 當把data中的name修改之后,此時會觸發name的 setter
中的 dep.notify
通知依賴本data的render watcher去 update
,update
會把flushSchedulerQueue
函數傳遞給 nextTick
,render watcher在 flushSchedulerQueue
函數運行時 watcher.run
再走 diff -> patch
那一套重渲染 re-render
視圖,這個過程中會重新依賴收集,這個過程是異步的;所以當我們直接修改了name之后打印,這時異步的改動還沒有被 patch
到視圖上,所以獲取視圖上的DOM元素還是原來的內容。
setter前: setter前為什么還打印原來的是原來內容呢,是因為 nextTick
在被調用的時候把回調挨個push進callbacks數組,之后執行的時候也是 for
循環出來挨個執行,所以是類似于隊列這樣一個概念,先入先出;在修改name之后,觸發把render watcher填入 schedulerQueue
隊列并把他的執行函數 flushSchedulerQueue
傳遞給nextTick
,此時callbacks隊列中已經有了 setter前函數
了,因為這個 cb
是在 setter前函數
之后被push進callbacks隊列的,那么先入先出的執行callbacks中回調的時候先執行 setter前函數
,這時并未執行render watcher的 watcher.run
,所以打印DOM元素仍然是原來的內容。
setter后: setter后這時已經執行完 flushSchedulerQueue
,這時render watcher已經把改動 patch
到視圖上,所以此時獲取DOM是改過之后的內容。
Promise方式: 相當于 Promise.then
的方式執行這個函數,此時DOM已經更改。
setTimeout方式: 最后執行macro task的任務,此時DOM已經更改。
注意,在執行 setter前函數
這個異步任務之前,同步的代碼已經執行完畢,異步的任務都還未執行,所有的 $nextTick
函數也執行完畢,所有回調都被push進了callbacks隊列中等待執行,所以在setter前函數
執行的時候,此時callbacks隊列是這樣的:[setter前函數
,flushSchedulerQueue
,setter后函數
,Promise方式函數
],它是一個micro task隊列,執行完畢之后執行macro task setTimeout
,所以打印出上面的結果。
另外,如果瀏覽器的宏任務隊列里面有setImmediate
、MessageChannel
、setTimeout/setInterval
各種類型的任務,那么會按照上面的順序挨個按照添加進event loop中的順序執行,所以如果瀏覽器支持MessageChannel
, nextTick
執行的是macroTimerFunc
,那么如果 macrotask queue 中同時有 nextTick
添加的任務和用戶自己添加的 setTimeout
類型的任務,會優先執行 nextTick
中的任務,因為MessageChannel
的優先級比 setTimeout
的高,setImmediate
同理。
關于“Vue異步更新機制和nextTick原理實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。