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