vue頁(yè)面渲染是異步的。vue采用的是異步渲染,這樣可以提升性能;如果不采用異步更新,在每次更新數(shù)據(jù)都會(huì)對(duì)當(dāng)前組件進(jìn)行重新渲染,為了性能考慮,Vue會(huì)在本輪數(shù)據(jù)更新后,再去異步更新視圖。
前端(vue)入門到精通課程,老師在線輔導(dǎo):聯(lián)系老師
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點(diǎn)擊使用
本教程操作環(huán)境:windows7系統(tǒng)、vue3版,DELL G3電腦。
vue頁(yè)面渲染是異步的。
Vue
在更新DOM
時(shí)是異步執(zhí)行的,只要偵聽(tīng)到數(shù)據(jù)變化,Vue
將開(kāi)啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更,如果同一個(gè)watcher
被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次,這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對(duì)于避免不必要的計(jì)算和DOM
操作是非常重要的,然后,在下一個(gè)的事件循環(huán)tick
中,Vue
刷新隊(duì)列并執(zhí)行實(shí)際(已去重的)工作,Vue
在內(nèi)部對(duì)異步隊(duì)列嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,如果執(zhí)行環(huán)境不支持,則會(huì)采用setTimeout(fn, 0)
代替。(學(xué)習(xí)視頻分享:vuejs入門教程、編程基礎(chǔ)視頻)
描述
對(duì)于Vue
為何采用異步渲染,簡(jiǎn)單來(lái)說(shuō)就是為了提升性能,因?yàn)椴徊捎卯惒礁拢诿看胃聰?shù)據(jù)都會(huì)對(duì)當(dāng)前組件進(jìn)行重新渲染,為了性能考慮,Vue
會(huì)在本輪數(shù)據(jù)更新后,再去異步更新視圖,舉個(gè)例子,讓我們?cè)谝粋€(gè)方法內(nèi)重復(fù)更新一個(gè)值。
this.msg = 1; this.msg = 2; this.msg = 3;
事實(shí)上,我們真正想要的其實(shí)只是最后一次更新而已,也就是說(shuō)前三次DOM
更新都是可以省略的,我們只需要等所有狀態(tài)都修改好了之后再進(jìn)行渲染就可以減少一些性能損耗。
對(duì)于渲染方面的問(wèn)題是很明確的,最終只渲染一次肯定比修改之后即渲染所耗費(fèi)的性能少,在這里我們還需要考慮一下異步更新隊(duì)列的相關(guān)問(wèn)題,假設(shè)我們現(xiàn)在是進(jìn)行了相關(guān)處理使得每次更新數(shù)據(jù)只進(jìn)行一次真實(shí)DOM
渲染,來(lái)讓我們考慮異步更新隊(duì)列的性能優(yōu)化。
假設(shè)這里是同步更新隊(duì)列,this.msg=1
,大致會(huì)發(fā)生這些事: msg
值更新 ->
觸發(fā)setter
->
觸發(fā)Watcher
的update
->
重新調(diào)用 render
->
生成新的vdom -> dom-diff -> dom
更新,這里的dom
更新并不是渲染(即布局、繪制、合成等一系列步驟),而是更新內(nèi)存中的DOM
樹(shù)結(jié)構(gòu),之后再運(yùn)行this.msg=2
,再重復(fù)上述步驟,之后的第3
次更新同樣會(huì)觸發(fā)相同的流程,等開(kāi)始渲染的時(shí)候,最新的DOM
樹(shù)中確實(shí)只會(huì)存在更新完成3
,從這里來(lái)看,前2
次對(duì)msg
的操作以及Vue
內(nèi)部對(duì)它的處理都是無(wú)用的操作,可以進(jìn)行優(yōu)化處理。
如果是異步更新隊(duì)列,會(huì)是下面的情況,運(yùn)行this.msg=1
,并不是立即進(jìn)行上面的流程,而是將對(duì)msg
有依賴的Watcher
都保存在隊(duì)列中,該隊(duì)列可能這樣[Watcher1, Watcher2...]
,當(dāng)運(yùn)行this.msg=2
后,同樣是將對(duì)msg
有依賴的Watcher
保存到隊(duì)列中,Vue
內(nèi)部會(huì)做去重判斷,這次操作后,可以認(rèn)為隊(duì)列數(shù)據(jù)沒(méi)有發(fā)生變化,第3
次更新也是上面的過(guò)程,當(dāng)然,你不可能只對(duì)msg
有操作,你可能對(duì)該組件中的另一個(gè)屬性也有操作,比如this.otherMsg=othermessage
,同樣會(huì)把對(duì)otherMsg
有依賴的Watcher
添加到異步更新隊(duì)列中,因?yàn)橛兄貜?fù)判斷操作,這個(gè)Watcher
也只會(huì)在隊(duì)列中存在一次,本次異步任務(wù)執(zhí)行結(jié)束后,會(huì)進(jìn)入下一個(gè)任務(wù)執(zhí)行流程,其實(shí)就是遍歷異步更新隊(duì)列中的每一個(gè)Watcher
,觸發(fā)其update
,然后進(jìn)行重新調(diào)用render
->
new vdom
->
dom-diff
->
dom
更新等流程,但是這種方式和同步更新隊(duì)列相比,不管操作多少次msg
, Vue
在內(nèi)部只會(huì)進(jìn)行一次重新調(diào)用真實(shí)更新流程,所以,對(duì)于異步更新隊(duì)列不是節(jié)省了渲染成本,而是節(jié)省了Vue
內(nèi)部計(jì)算及DOM
樹(shù)操作的成本,不管采用哪種方式,渲染確實(shí)只有一次。
此外,組件內(nèi)部實(shí)際使用VirtualDOM
進(jìn)行渲染,也就是說(shuō),組件內(nèi)部其實(shí)是不關(guān)心哪個(gè)狀態(tài)發(fā)生了變化,它只需要計(jì)算一次就可以得知哪些節(jié)點(diǎn)需要更新,也就是說(shuō),如果更改了N
個(gè)狀態(tài),其實(shí)只需要發(fā)送一個(gè)信號(hào)就可以將DOM
更新到最新,如果我們更新多個(gè)值。
this.msg = 1; this.age = 2; this.name = 3;
此處我們分三次修改了三種狀態(tài),但其實(shí)Vue
只會(huì)渲染一次,因?yàn)?code>VIrtualDOM只需要一次就可以將整個(gè)組件的DOM
更新到最新,它根本不會(huì)關(guān)心這個(gè)更新的信號(hào)到底是從哪個(gè)具體的狀態(tài)發(fā)出來(lái)的。
而為了達(dá)到這個(gè)目的,我們需要將渲染操作推遲到所有的狀態(tài)都修改完成,為了做到這一點(diǎn)只需要將渲染操作推遲到本輪事件循環(huán)的最后或者下一輪事件循環(huán),也就是說(shuō),只需要在本輪事件循環(huán)的最后,等前面更新?tīng)顟B(tài)的語(yǔ)句都執(zhí)行完之后,執(zhí)行一次渲染操作,它就可以無(wú)視前面各種更新?tīng)顟B(tài)的語(yǔ)法,無(wú)論前面寫(xiě)了多少條更新?tīng)顟B(tài)的語(yǔ)句,只在最后渲染一次就可以了。
將渲染推遲到本輪事件循環(huán)的最后執(zhí)行渲染的時(shí)機(jī)會(huì)比推遲到下一輪快很多,所以Vue
優(yōu)先將渲染操作推遲到本輪事件循環(huán)的最后,如果執(zhí)行環(huán)境不支持會(huì)降級(jí)到下一輪,Vue
的變化偵測(cè)機(jī)制(setter
)決定了它必然會(huì)在每次狀態(tài)發(fā)生變化時(shí)都會(huì)發(fā)出渲染的信號(hào),但Vue
會(huì)在收到信號(hào)之后檢查隊(duì)列中是否已經(jīng)存在這個(gè)任務(wù),保證隊(duì)列中不會(huì)有重復(fù),如果隊(duì)列中不存在則將渲染操作添加到隊(duì)列中,之后通過(guò)異步的方式延遲執(zhí)行隊(duì)列中的所有渲染的操作并清空隊(duì)列,當(dāng)同一輪事件循環(huán)中反復(fù)修改狀態(tài)時(shí),并不會(huì)反復(fù)向隊(duì)列中添加相同的渲染操作,所以我們?cè)谑褂?code>Vue時(shí),修改狀態(tài)后更新DOM
都是異步的。
當(dāng)數(shù)據(jù)變化后會(huì)調(diào)用notify
方法,將watcher
遍歷,調(diào)用update
方法通知watcher
進(jìn)行更新,這時(shí)候watcher
并不會(huì)立即去執(zhí)行,在update
中會(huì)調(diào)用queueWatcher
方法將watcher
放到了一個(gè)隊(duì)列里,在queueWatcher
會(huì)根據(jù)watcher
的進(jìn)行去重,若多個(gè)屬性依賴一個(gè)watcher
,則如果隊(duì)列中沒(méi)有該watcher
就會(huì)將該watcher
添加到隊(duì)列中,然后便會(huì)在$nextTick
方法的執(zhí)行隊(duì)列中加入一個(gè)flushSchedulerQueue
方法(這個(gè)方法將會(huì)觸發(fā)在緩沖隊(duì)列的所有回調(diào)的執(zhí)行),然后將$nextTick
方法的回調(diào)加入$nextTick
方法中維護(hù)的執(zhí)行隊(duì)列,flushSchedulerQueue
中開(kāi)始會(huì)觸發(fā)一個(gè)before
的方法,其實(shí)就是beforeUpdate
,然后watcher.run
()才開(kāi)始真正執(zhí)行watcher
,執(zhí)行完頁(yè)面就渲染完成,更新完成后會(huì)調(diào)用updated
鉤子。
$nextTick
在上文中談到了對(duì)于Vue
為何采用異步渲染,假如此時(shí)我們有一個(gè)需求,需要在頁(yè)面渲染完成后取得頁(yè)面的DOM
元素,而由于渲染是異步的,我們不能直接在定義的方法中同步取得這個(gè)值的,于是就有了vm.$nextTick
方法,Vue
中$nextTick
方法將回調(diào)延遲到下次DOM
更新循環(huán)之后執(zhí)行,也就是在下次DOM
更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào),在修改數(shù)據(jù)之后立即使用這個(gè)方法,能夠獲取更新后的DOM
。簡(jiǎn)單來(lái)說(shuō)就是當(dāng)數(shù)據(jù)更新時(shí),在DOM
中渲染完成后,執(zhí)行回調(diào)函數(shù)。
通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)演示$nextTick
方法的作用,首先需要知道Vue
在更新DOM
時(shí)是異步執(zhí)行的,也就是說(shuō)在更新數(shù)據(jù)時(shí)其不會(huì)阻塞代碼的執(zhí)行,直到執(zhí)行棧中代碼執(zhí)行結(jié)束之后,才開(kāi)始執(zhí)行異步任務(wù)隊(duì)列的代碼,所以在數(shù)據(jù)更新時(shí),組件不會(huì)立即渲染,此時(shí)在獲取到DOM
結(jié)構(gòu)后取得的值依然是舊的值,而在$nextTick
方法中設(shè)定的回調(diào)函數(shù)會(huì)在組件渲染完成之后執(zhí)行,取得DOM
結(jié)構(gòu)后取得的值便是新的值。
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; console.log("DOM未更新:", this.$refs.msgElement.innerHTML) this.$nextTick(() => { console.log("DOM已更新:", this.$refs.msgElement.innerHTML) }) } }, }) </script> </html>
異步機(jī)制
官方文檔中說(shuō)明,Vue
在更新DOM
時(shí)是異步執(zhí)行的,只要偵聽(tīng)到數(shù)據(jù)變化,Vue
將開(kāi)啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更,如果同一個(gè)watcher
被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對(duì)于避免不必要的計(jì)算和DOM
操作是非常重要的。然后,在下一個(gè)的事件循環(huán)tick
中,Vue
刷新隊(duì)列并執(zhí)行實(shí)際工作。Vue
在內(nèi)部對(duì)異步隊(duì)列嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,如果執(zhí)行環(huán)境不支持,則會(huì)采用 setTimeout(fn, 0)
代替。Js
是單線程的,其引入了同步阻塞與異步非阻塞的執(zhí)行模式,在Js
異步模式中維護(hù)了一個(gè)Event Loop
,Event Loop
是一個(gè)執(zhí)行模型,在不同的地方有不同的實(shí)現(xiàn),瀏覽器和NodeJS
基于不同的技術(shù)實(shí)現(xiàn)了各自的Event Loop
。瀏覽器的Event Loop
是在HTML5
的規(guī)范中明確定義,NodeJS
的Event Loop
是基于libuv
實(shí)現(xiàn)的。
在瀏覽器中的Event Loop
由執(zhí)行棧Execution Stack
、后臺(tái)線程Background Threads
、宏隊(duì)列Macrotask Queue
、微隊(duì)列Microtask Queue
組成。
- 執(zhí)行棧就是在主線程執(zhí)行同步任務(wù)的數(shù)據(jù)結(jié)構(gòu),函數(shù)調(diào)用形成了一個(gè)由若干幀組成的棧。
- 后臺(tái)線程就是瀏覽器實(shí)現(xiàn)對(duì)于
setTimeout
、setInterval
、XMLHttpRequest
等等的執(zhí)行線程。 - 宏隊(duì)列,一些異步任務(wù)的回調(diào)會(huì)依次進(jìn)入宏隊(duì)列,等待后續(xù)被調(diào)用,包括
setTimeout
、setInterval
、setImmediate(Node)
、requestAnimationFrame
、UI rendering
、I/O
等操作。 - 微隊(duì)列,另一些異步任務(wù)的回調(diào)會(huì)依次進(jìn)入微隊(duì)列,等待后續(xù)調(diào)用,包括
Promise
、process.nextTick(Node)
、Object.observe
、MutationObserver
等操作。
當(dāng)Js
執(zhí)行時(shí),進(jìn)行如下流程:
-
首先將執(zhí)行棧中代碼同步執(zhí)行,將這些代碼中異步任務(wù)加入后臺(tái)線程中。
-
執(zhí)行棧中的同步代碼執(zhí)行完畢后,執(zhí)行棧清空,并開(kāi)始掃描微隊(duì)列。
-
取出微隊(duì)列隊(duì)首任務(wù),放入執(zhí)行棧中執(zhí)行,此時(shí)微隊(duì)列是進(jìn)行了出隊(duì)操作。
-
當(dāng)執(zhí)行棧執(zhí)行完成后,繼續(xù)出隊(duì)微隊(duì)列任務(wù)并執(zhí)行,直到微隊(duì)列任務(wù)全部執(zhí)行完畢。
-
最后一個(gè)微隊(duì)列任務(wù)出隊(duì)并進(jìn)入執(zhí)行棧后微隊(duì)列中任務(wù)為空,當(dāng)執(zhí)行棧任務(wù)完成后,開(kāi)始掃面微隊(duì)列為空,繼續(xù)掃描宏隊(duì)列任務(wù),宏隊(duì)列出隊(duì),放入執(zhí)行棧中執(zhí)行,執(zhí)行完畢后繼續(xù)掃描微隊(duì)列為空則掃描宏隊(duì)列,出隊(duì)執(zhí)行。
-
不斷往復(fù)
...
。
實(shí)例
// Step 1 console.log(1); // Step 2 setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0); // Step 3 new Promise((resolve, reject) => { console.log(4); resolve(); }).then(() => { console.log(5); }) // Step 4 setTimeout(() => { console.log(6); }, 0); // Step 5 console.log(7); // Step N // ... // Result /* 1 4 7 5 2 3 6 */
Step 1
// 執(zhí)行棧 console // 微隊(duì)列 [] // 宏隊(duì)列 [] console.log(1); // 1
Step 2
// 執(zhí)行棧 setTimeout // 微隊(duì)列 [] // 宏隊(duì)列 [setTimeout1] setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0);
Step 3
// 執(zhí)行棧 Promise // 微隊(duì)列 [then1] // 宏隊(duì)列 [setTimeout1] new Promise((resolve, reject) => { console.log(4); // 4 // Promise是個(gè)函數(shù)對(duì)象,此處是同步執(zhí)行的 // 執(zhí)行棧 Promise console resolve(); }).then(() => { console.log(5); })
Step 4
// 執(zhí)行棧 setTimeout // 微隊(duì)列 [then1] // 宏隊(duì)列 [setTimeout1 setTimeout2] setTimeout(() => { console.log(6); }, 0);
Step 5
// 執(zhí)行棧 console // 微隊(duì)列 [then1] // 宏隊(duì)列 [setTimeout1 setTimeout2] console.log(7); // 7
Step 6
// 執(zhí)行棧 then1 // 微隊(duì)列 [] // 宏隊(duì)列 [setTimeout1 setTimeout2] console.log(5); // 5
Step 7
// 執(zhí)行棧 setTimeout1 // 微隊(duì)列 [then2] // 宏隊(duì)列 [setTimeout2] console.log(2); // 2 Promise.resolve().then(() => { console.log(3); });
Step 8
// 執(zhí)行棧 then2 // 微隊(duì)列 [] // 宏隊(duì)列 [setTimeout2] console.log(3); // 3
Step 9
// 執(zhí)行棧 setTimeout2 // 微隊(duì)列 [] // 宏隊(duì)列 [] console.log(6); // 6
分析
在了解異步任務(wù)的執(zhí)行隊(duì)列后,回到中$nextTick
方法,當(dāng)用戶數(shù)據(jù)更新時(shí),Vue
將會(huì)維護(hù)一個(gè)緩沖隊(duì)列,對(duì)于所有的更新數(shù)據(jù)將要進(jìn)行的組件渲染與DOM
操作進(jìn)行一定的策略處理后加入緩沖隊(duì)列,然后便會(huì)在$nextTick
方法的執(zhí)行隊(duì)列中加入一個(gè)flushSchedulerQueue
方法(這個(gè)方法將會(huì)觸發(fā)在緩沖隊(duì)列的所有回調(diào)的執(zhí)行),然后將$nextTick
方法的回調(diào)加入$nextTick
方法中維護(hù)的執(zhí)行隊(duì)列,在異步掛載的執(zhí)行隊(duì)列觸發(fā)時(shí)就會(huì)首先會(huì)首先執(zhí)行flushSchedulerQueue
方法來(lái)處理DOM
渲染的任務(wù),然后再去執(zhí)行$nextTick
方法構(gòu)建的任務(wù),這樣就可以實(shí)現(xiàn)在$nextTick
方法中取得已渲染完成的DOM
結(jié)構(gòu)。在測(cè)試的過(guò)程中發(fā)現(xiàn)了一個(gè)很有意思的現(xiàn)象,在上述例子中的加入兩個(gè)按鈕,在點(diǎn)擊updateMsg
按鈕的結(jié)果是3 2 1
,點(diǎn)擊updateMsgTest
按鈕的運(yùn)行結(jié)果是2 3 1
。
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> <button @click="updateMsgTest">updateMsgTest</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) }, updateMsgTest: function(){ setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) } }, }) </script> </html>
這里假設(shè)運(yùn)行環(huán)境中Promise
對(duì)象是完全支持的,那么使用setTimeout
是宏隊(duì)列在最后執(zhí)行這個(gè)是沒(méi)有異議的,但是使用$nextTick
方法以及自行定義的Promise
實(shí)例是有執(zhí)行順序的問(wèn)題的,雖然都是微隊(duì)列任務(wù),但是在Vue
中具體實(shí)現(xiàn)的原因?qū)е铝藞?zhí)行順序可能會(huì)有所不同,首先直接看一下$nextTick
方法的源碼,關(guān)鍵地方添加了注釋,請(qǐng)注意這是Vue2.4.2
版本的源碼,在后期$nextTick
方法可能有所變更。
/** * Defer a task to execute it asynchronously. */ var nextTick = (function () { // 閉包 內(nèi)部變量 var callbacks = []; // 執(zhí)行隊(duì)列 var pending = false; // 標(biāo)識(shí),用以判斷在某個(gè)事件循環(huán)中是否為第一次加入,第一次加入的時(shí)候才觸發(fā)異步執(zhí)行的隊(duì)列掛載 var timerFunc; // 以何種方法執(zhí)行掛載異步執(zhí)行隊(duì)列,這里假設(shè)Promise是完全支持的 function nextTickHandler () { // 異步掛載的執(zhí)行任務(wù),觸發(fā)時(shí)就已經(jīng)正式準(zhǔn)備開(kāi)始執(zhí)行異步任務(wù)了 pending = false; // 標(biāo)識(shí)置false var copies = callbacks.slice(0); // 創(chuàng)建副本 callbacks.length = 0; // 執(zhí)行隊(duì)列置空 for (var i = 0; i < copies.length; i++) { copies[i](); // 執(zhí)行 } } // the nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore if */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = function (err) { console.error(err); }; timerFunc = function () { p.then(nextTickHandler).catch(logError); // 掛載異步任務(wù)隊(duì)列 // in problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) { setTimeout(noop); } }; } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // use MutationObserver where native Promise is not available, // e.g. PhantomJS IE11, iOS7, Android 4.4 var counter = 1; var observer = new MutationObserver(nextTickHandler); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // fallback to setTimeout /* istanbul ignore next */ timerFunc = function () { setTimeout(nextTickHandler, 0); }; } return function queueNextTick (cb, ctx) { // nextTick方法真正導(dǎo)出的方法 var _resolve; callbacks.push(function () { // 添加到執(zhí)行隊(duì)列中 并加入異常處理 if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); //判斷在當(dāng)前事件循環(huán)中是否為第一次加入,若是第一次加入則置標(biāo)識(shí)為true并執(zhí)行timerFunc函數(shù)用以掛載執(zhí)行隊(duì)列到Promise // 這個(gè)標(biāo)識(shí)在執(zhí)行隊(duì)列中的任務(wù)將要執(zhí)行時(shí)便置為false并創(chuàng)建執(zhí)行隊(duì)列的副本去運(yùn)行執(zhí)行隊(duì)列中的任務(wù),參見(jiàn)nextTickHandler函數(shù)的實(shí)現(xiàn) // 在當(dāng)前事件循環(huán)中置標(biāo)識(shí)true并掛載,然后再次調(diào)用nextTick方法時(shí)只是將任務(wù)加入到執(zhí)行隊(duì)列中,直到掛載的異步任務(wù)觸發(fā),便置標(biāo)識(shí)為false然后執(zhí)行任務(wù),再次調(diào)用nextTick方法時(shí)就是同樣的執(zhí)行方式然后不斷如此往復(fù) if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })();
回到剛才提出的問(wèn)題上,在更新DOM
操作時(shí)會(huì)先觸發(fā)$nextTick
方法的回調(diào),解決這個(gè)問(wèn)題的關(guān)鍵在于誰(shuí)先將異步任務(wù)掛載到Promise
對(duì)象上。
首先對(duì)有數(shù)據(jù)更新的updateMsg
按鈕觸發(fā)的方法進(jìn)行debug
,斷點(diǎn)設(shè)置在Vue.js
的715
行,版本為2.4.2
,在查看調(diào)用棧以及傳入的參數(shù)時(shí)可以觀察到第一次執(zhí)行$nextTick
方法的其實(shí)是由于數(shù)據(jù)更新而調(diào)用的nextTick(flushSchedulerQueue);
語(yǔ)句,也就是說(shuō)在執(zhí)行this.msg = "Update";
的時(shí)候就已經(jīng)觸發(fā)了第一次的$nextTick
方法,此時(shí)在$nextTick
方法中的任務(wù)隊(duì)列會(huì)首先將flushSchedulerQueue
方法加入隊(duì)列并掛載$nextTick
方法的執(zhí)行隊(duì)列到Promise
對(duì)象上,然后才是自行自定義的Promise.resolve().then(() => console.log(2))
語(yǔ)句的掛載,當(dāng)執(zhí)行微任務(wù)隊(duì)列中的任務(wù)時(shí),首先會(huì)執(zhí)行第一個(gè)掛載到Promise
的任務(wù),此時(shí)這個(gè)任務(wù)是運(yùn)行執(zhí)行隊(duì)列,這個(gè)隊(duì)列中有兩個(gè)方法,首先會(huì)運(yùn)行flushSchedulerQueue
方法去觸發(fā)組件的DOM
渲染操作,然后再執(zhí)行console.log(3)
,然后執(zhí)行第二個(gè)微隊(duì)列的任務(wù)也就是() => console.log(2)
,此時(shí)微任務(wù)隊(duì)列清空,然后再去宏任務(wù)隊(duì)列執(zhí)行console.log(1)
。
接下來(lái)對(duì)于沒(méi)有數(shù)據(jù)更新的updateMsgTest
按鈕觸發(fā)的方法進(jìn)行debug
,斷點(diǎn)設(shè)置在同樣的位置,此時(shí)沒(méi)有數(shù)據(jù)更新,那么第一次觸發(fā)$nextTick
方法的是自行定義的回調(diào)函數(shù),那么此時(shí)$nextTick
方法的執(zhí)行隊(duì)列才會(huì)被掛載到Promise
對(duì)象上,很顯然在此之前自行定義的輸出2
的Promise
回調(diào)已經(jīng)被掛載,那么對(duì)于這個(gè)按鈕綁定的方法的執(zhí)行流程便是首先執(zhí)行console.log(2)
,然后執(zhí)行$nextTick
方法閉包的執(zhí)行隊(duì)列,此時(shí)執(zhí)行隊(duì)列中只有一個(gè)回調(diào)函數(shù)console.log(3)
,此時(shí)微任務(wù)隊(duì)列清空,然后再去宏任務(wù)隊(duì)列執(zhí)行console.log(1)
。
簡(jiǎn)單來(lái)說(shuō)就是誰(shuí)先掛載Promise
對(duì)象的問(wèn)題,在調(diào)用$nextTick
方法時(shí)就會(huì)將其閉包內(nèi)部維護(hù)的執(zhí)行隊(duì)列掛載到Promise
對(duì)象,在數(shù)據(jù)更新時(shí)Vue
內(nèi)部首先就會(huì)執(zhí)行$nextTick
方法,之后便將執(zhí)行隊(duì)列掛載到了Promise
對(duì)象上,其實(shí)在明白Js
的Event Loop
模型后,將數(shù)據(jù)更新也看做一個(gè)$nextTick
方法的調(diào)用,并且明白$nextTick
方法會(huì)一次性執(zhí)行所有推入的回調(diào),就可以明白其執(zhí)行順序的問(wèn)題了,下面是一個(gè)關(guān)于$nextTick
方法的最小化的DEMO
。
var nextTick = (function(){ var pending = false; const callback = []; var p = Promise.resolve(); var handler = function(){ pending = true; callback.forEach(fn => fn()); } var timerFunc = function(){ p.then(handler); } return function queueNextTick(fn){ callback.push(() => fn()); if(!pending){ pending = true; timerFunc(); } } })(); (function(){ nextTick(() => console.log("觸發(fā)DOM渲染隊(duì)列的方法")); // 注釋 / 取消注釋 來(lái)查看效果 setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) nextTick(() => { console.log(3) }) })();
(學(xué)習(xí)視頻分享:vuejs入門教程、編程基礎(chǔ)視頻)