Vue采用異步渲染的原理分析
Vue為何采用異步渲染
Vue
在更新DOM
時是異步執(zhí)行的,只要偵聽到數(shù)據(jù)變化,Vue
將開啟一個隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更,如果同一個watcher
被多次觸發(fā),只會被推入到隊(duì)列中一次,這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計(jì)算和DOM
操作是非常重要的,然后,在下一個的事件循環(huán)tick
中,Vue
刷新隊(duì)列并執(zhí)行實(shí)際(已去重的)工作,Vue
在內(nèi)部對異步隊(duì)列嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,如果執(zhí)行環(huán)境不支持,則會采用setTimeout(fn, 0)
代替。
描述
對于Vue
為何采用異步渲染,簡單來說就是為了提升性能,因?yàn)椴徊捎卯惒礁拢诿看胃聰?shù)據(jù)都會對當(dāng)前組件進(jìn)行重新渲染,為了性能考慮,Vue
會在本輪數(shù)據(jù)更新后,再去異步更新視圖,舉個例子,讓我們在一個方法內(nèi)重復(fù)更新一個值。
this.msg = 1; this.msg = 2; this.msg = 3;
事實(shí)上,我們真正想要的其實(shí)只是最后一次更新而已,也就是說前三次DOM
更新都是可以省略的,我們只需要等所有狀態(tài)都修改好了之后再進(jìn)行渲染就可以減少一些性能損耗。
對于渲染方面的問題是很明確的,最終只渲染一次肯定比修改之后即渲染所耗費(fèi)的性能少,在這里我們還需要考慮一下異步更新隊(duì)列的相關(guān)問題,假設(shè)我們現(xiàn)在是進(jìn)行了相關(guān)處理使得每次更新數(shù)據(jù)只進(jìn)行一次真實(shí)DOM
渲染,來讓我們考慮異步更新隊(duì)列的性能優(yōu)化。
假設(shè)這里是同步更新隊(duì)列,this.msg=1
,大致會發(fā)生這些事: msg
值更新 ->
觸發(fā)setter
->
觸發(fā)Watcher
的update
->
重新調(diào)用 render
->
生成新的vdom -> dom-diff -> dom
更新,這里的dom
更新并不是渲染(即布局、繪制、合成等一系列步驟),而是更新內(nèi)存中的DOM
樹結(jié)構(gòu),之后再運(yùn)行this.msg=2
,再重復(fù)上述步驟,之后的第3
次更新同樣會觸發(fā)相同的流程,等開始渲染的時候,最新的DOM
樹中確實(shí)只會存在更新完成3
,從這里來看,前2
次對msg
的操作以及Vue
內(nèi)部對它的處理都是無用的操作,可以進(jìn)行優(yōu)化處理。
如果是異步更新隊(duì)列,會是下面的情況,運(yùn)行this.msg=1
,并不是立即進(jìn)行上面的流程,而是將對msg
有依賴的Watcher
都保存在隊(duì)列中,該隊(duì)列可能這樣[Watcher1, Watcher2...]
,當(dāng)運(yùn)行this.msg=2
后,同樣是將對msg
有依賴的Watcher
保存到隊(duì)列中,Vue
內(nèi)部會做去重判斷,這次操作后,可以認(rèn)為隊(duì)列數(shù)據(jù)沒有發(fā)生變化,第3
次更新也是上面的過程,當(dāng)然,你不可能只對msg
有操作,你可能對該組件中的另一個屬性也有操作,比如this.otherMsg=othermessage
,同樣會把對otherMsg
有依賴的Watcher
添加到異步更新隊(duì)列中,因?yàn)橛兄貜?fù)判斷操作,這個Watcher
也只會在隊(duì)列中存在一次,本次異步任務(wù)執(zhí)行結(jié)束后,會進(jìn)入下一個任務(wù)執(zhí)行流程,其實(shí)就是遍歷異步更新隊(duì)列中的每一個Watcher
,觸發(fā)其update
,然后進(jìn)行重新調(diào)用render
->
new vdom
->
dom-diff
->
dom
更新等流程,但是這種方式和同步更新隊(duì)列相比,不管操作多少次msg
, Vue
在內(nèi)部只會進(jìn)行一次重新調(diào)用真實(shí)更新流程,所以,對于異步更新隊(duì)列不是節(jié)省了渲染成本,而是節(jié)省了Vue
內(nèi)部計(jì)算及DOM
樹操作的成本,不管采用哪種方式,渲染確實(shí)只有一次。
此外,組件內(nèi)部實(shí)際使用VirtualDOM
進(jìn)行渲染,也就是說,組件內(nèi)部其實(shí)是不關(guān)心哪個狀態(tài)發(fā)生了變化,它只需要計(jì)算一次就可以得知哪些節(jié)點(diǎn)需要更新,也就是說,如果更改了N
個狀態(tài),其實(shí)只需要發(fā)送一個信號就可以將DOM
更新到最新,如果我們更新多個值。
this.msg = 1; this.age = 2; this.name = 3;
此處我們分三次修改了三種狀態(tài),但其實(shí)Vue
只會渲染一次,因?yàn)?code>VIrtualDOM只需要一次就可以將整個組件的DOM
更新到最新,它根本不會關(guān)心這個更新的信號到底是從哪個具體的狀態(tài)發(fā)出來的。
而為了達(dá)到這個目的,我們需要將渲染操作推遲到所有的狀態(tài)都修改完成,為了做到這一點(diǎn)只需要將渲染操作推遲到本輪事件循環(huán)的最后或者下一輪事件循環(huán),也就是說,只需要在本輪事件循環(huán)的最后,等前面更新狀態(tài)的語句都執(zhí)行完之后,執(zhí)行一次渲染操作,它就可以無視前面各種更新狀態(tài)的語法,無論前面寫了多少條更新狀態(tài)的語句,只在最后渲染一次就可以了。
將渲染推遲到本輪事件循環(huán)的最后執(zhí)行渲染的時機(jī)會比推遲到下一輪快很多,所以Vue
優(yōu)先將渲染操作推遲到本輪事件循環(huán)的最后,如果執(zhí)行環(huán)境不支持會降級到下一輪,Vue
的變化偵測機(jī)制(setter
)決定了它必然會在每次狀態(tài)發(fā)生變化時都會發(fā)出渲染的信號,但Vue
會在收到信號之后檢查隊(duì)列中是否已經(jīng)存在這個任務(wù),保證隊(duì)列中不會有重復(fù),如果隊(duì)列中不存在則將渲染操作添加到隊(duì)列中,之后通過異步的方式延遲執(zhí)行隊(duì)列中的所有渲染的操作并清空隊(duì)列,當(dāng)同一輪事件循環(huán)中反復(fù)修改狀態(tài)時,并不會反復(fù)向隊(duì)列中添加相同的渲染操作,所以我們在使用Vue
時,修改狀態(tài)后更新DOM
都是異步的。
當(dāng)數(shù)據(jù)變化后會調(diào)用notify
方法,將watcher
遍歷,調(diào)用update
方法通知watcher
進(jìn)行更新,這時候watcher
并不會立即去執(zhí)行,在update
中會調(diào)用queueWatcher
方法將watcher
放到了一個隊(duì)列里,在queueWatcher
會根據(jù)watcher
的進(jìn)行去重,若多個屬性依賴一個watcher
,則如果隊(duì)列中沒有該watcher
就會將該watcher
添加到隊(duì)列中,然后便會在$nextTick
方法的執(zhí)行隊(duì)列中加入一個flushSchedulerQueue
方法(這個方法將會觸發(fā)在緩沖隊(duì)列的所有回調(diào)的執(zhí)行),然后將$nextTick
方法的回調(diào)加入$nextTick
方法中維護(hù)的執(zhí)行隊(duì)列,flushSchedulerQueue
中開始會觸發(fā)一個before
的方法,其實(shí)就是beforeUpdate
,然后watcher.run
()才開始真正執(zhí)行watcher
,執(zhí)行完頁面就渲染完成,更新完成后會調(diào)用updated
鉤子。
$nextTick
在上文中談到了對于Vue
為何采用異步渲染,假如此時我們有一個需求,需要在頁面渲染完成后取得頁面的DOM
元素,而由于渲染是異步的,我們不能直接在定義的方法中同步取得這個值的,于是就有了vm.$nextTick
方法,Vue
中$nextTick
方法將回調(diào)延遲到下次DOM
更新循環(huán)之后執(zhí)行,也就是在下次DOM
更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào),在修改數(shù)據(jù)之后立即使用這個方法,能夠獲取更新后的DOM
。簡單來說就是當(dāng)數(shù)據(jù)更新時,在DOM
中渲染完成后,執(zhí)行回調(diào)函數(shù)。
通過一個簡單的例子來演示$nextTick
方法的作用,首先需要知道Vue
在更新DOM
時是異步執(zhí)行的,也就是說在更新數(shù)據(jù)時其不會阻塞代碼的執(zhí)行,直到執(zhí)行棧中代碼執(zhí)行結(jié)束之后,才開始執(zhí)行異步任務(wù)隊(duì)列的代碼,所以在數(shù)據(jù)更新時,組件不會立即渲染,此時在獲取到DOM
結(jié)構(gòu)后取得的值依然是舊的值,而在$nextTick
方法中設(shè)定的回調(diào)函數(shù)會在組件渲染完成之后執(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ī)制
官方文檔中說明,Vue
在更新DOM
時是異步執(zhí)行的,只要偵聽到數(shù)據(jù)變化,Vue
將開啟一個隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更,如果同一個watcher
被多次觸發(fā),只會被推入到隊(duì)列中一次。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計(jì)算和DOM
操作是非常重要的。然后,在下一個的事件循環(huán)tick
中,Vue
刷新隊(duì)列并執(zhí)行實(shí)際工作。Vue
在內(nèi)部對異步隊(duì)列嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,如果執(zhí)行環(huán)境不支持,則會采用 setTimeout(fn, 0)
代替。Js
是單線程的,其引入了同步阻塞與異步非阻塞的執(zhí)行模式,在Js
異步模式中維護(hù)了一個Event Loop
,Event Loop
是一個執(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
、后臺線程Background Threads
、宏隊(duì)列Macrotask Queue
、微隊(duì)列Microtask Queue
組成。
- 執(zhí)行棧就是在主線程執(zhí)行同步任務(wù)的數(shù)據(jù)結(jié)構(gòu),函數(shù)調(diào)用形成了一個由若干幀組成的棧。
- 后臺線程就是瀏覽器實(shí)現(xiàn)對于
setTimeout
、setInterval
、XMLHttpRequest
等等的執(zhí)行線程。 - 宏隊(duì)列,一些異步任務(wù)的回調(diào)會依次進(jìn)入宏隊(duì)列,等待后續(xù)被調(diào)用,包括
setTimeout
、setInterval
、setImmediate(Node)
、requestAnimationFrame
、UI rendering
、I/O
等操作。 - 微隊(duì)列,另一些異步任務(wù)的回調(diào)會依次進(jìn)入微隊(duì)列,等待后續(xù)調(diào)用,包括
Promise
、process.nextTick(Node)
、Object.observe
、MutationObserver
等操作。
當(dāng)Js
執(zhí)行時,進(jìn)行如下流程:
- 首先將執(zhí)行棧中代碼同步執(zhí)行,將這些代碼中異步任務(wù)加入后臺線程中。
- 執(zhí)行棧中的同步代碼執(zhí)行完畢后,執(zhí)行棧清空,并開始掃描微隊(duì)列。
- 取出微隊(duì)列隊(duì)首任務(wù),放入執(zhí)行棧中執(zhí)行,此時微隊(duì)列是進(jìn)行了出隊(duì)操作。
- 當(dāng)執(zhí)行棧執(zhí)行完成后,繼續(xù)出隊(duì)微隊(duì)列任務(wù)并執(zhí)行,直到微隊(duì)列任務(wù)全部執(zhí)行完畢。
- 最后一個微隊(duì)列任務(wù)出隊(duì)并進(jìn)入執(zhí)行棧后微隊(duì)列中任務(wù)為空,當(dāng)執(zhí)行棧任務(wù)完成后,開始掃面微隊(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是個函數(shù)對象,此處是同步執(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ù)更新時,Vue
將會維護(hù)一個緩沖隊(duì)列,對于所有的更新數(shù)據(jù)將要進(jìn)行的組件渲染與DOM
操作進(jìn)行一定的策略處理后加入緩沖隊(duì)列,然后便會在$nextTick
方法的執(zhí)行隊(duì)列中加入一個flushSchedulerQueue
方法(這個方法將會觸發(fā)在緩沖隊(duì)列的所有回調(diào)的執(zhí)行),然后將$nextTick
方法的回調(diào)加入$nextTick
方法中維護(hù)的執(zhí)行隊(duì)列,在異步掛載的執(zhí)行隊(duì)列觸發(fā)時就會首先會首先執(zhí)行flushSchedulerQueue
方法來處理DOM
渲染的任務(wù),然后再去執(zhí)行$nextTick
方法構(gòu)建的任務(wù),這樣就可以實(shí)現(xiàn)在$nextTick
方法中取得已渲染完成的DOM
結(jié)構(gòu)。在測試的過程中發(fā)現(xiàn)了一個很有意思的現(xiàn)象,在上述例子中的加入兩個按鈕,在點(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
對象是完全支持的,那么使用setTimeout
是宏隊(duì)列在最后執(zhí)行這個是沒有異議的,但是使用$nextTick
方法以及自行定義的Promise
實(shí)例是有執(zhí)行順序的問題的,雖然都是微隊(duì)列任務(wù),但是在Vue
中具體實(shí)現(xiàn)的原因?qū)е铝藞?zhí)行順序可能會有所不同,首先直接看一下$nextTick
方法的源碼,關(guān)鍵地方添加了注釋,請注意這是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)識,用以判斷在某個事件循環(huán)中是否為第一次加入,第一次加入的時候才觸發(fā)異步執(zhí)行的隊(duì)列掛載 var timerFunc; // 以何種方法執(zhí)行掛載異步執(zhí)行隊(duì)列,這里假設(shè)Promise是完全支持的 function nextTickHandler () { // 異步掛載的執(zhí)行任務(wù),觸發(fā)時就已經(jīng)正式準(zhǔn)備開始執(zhí)行異步任務(wù)了 pending = false; // 標(biāo)識置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)識為true并執(zhí)行timerFunc函數(shù)用以掛載執(zhí)行隊(duì)列到Promise // 這個標(biāo)識在執(zhí)行隊(duì)列中的任務(wù)將要執(zhí)行時便置為false并創(chuàng)建執(zhí)行隊(duì)列的副本去運(yùn)行執(zhí)行隊(duì)列中的任務(wù),參見nextTickHandler函數(shù)的實(shí)現(xiàn) // 在當(dāng)前事件循環(huán)中置標(biāo)識true并掛載,然后再次調(diào)用nextTick方法時只是將任務(wù)加入到執(zhí)行隊(duì)列中,直到掛載的異步任務(wù)觸發(fā),便置標(biāo)識為false然后執(zhí)行任務(wù),再次調(diào)用nextTick方法時就是同樣的執(zhí)行方式然后不斷如此往復(fù) if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })();
回到剛才提出的問題上,在更新DOM
操作時會先觸發(fā)$nextTick
方法的回調(diào),解決這個問題的關(guān)鍵在于誰先將異步任務(wù)掛載到Promise
對象上。
首先對有數(shù)據(jù)更新的updateMsg
按鈕觸發(fā)的方法進(jìn)行debug
,斷點(diǎn)設(shè)置在Vue.js
的715
行,版本為2.4.2
,在查看調(diào)用棧以及傳入的參數(shù)時可以觀察到第一次執(zhí)行$nextTick
方法的其實(shí)是由于數(shù)據(jù)更新而調(diào)用的nextTick(flushSchedulerQueue);
語句,也就是說在執(zhí)行this.msg = "Update";
的時候就已經(jīng)觸發(fā)了第一次的$nextTick
方法,此時在$nextTick
方法中的任務(wù)隊(duì)列會首先將flushSchedulerQueue
方法加入隊(duì)列并掛載$nextTick
方法的執(zhí)行隊(duì)列到Promise
對象上,然后才是自行自定義的Promise.resolve().then(() => console.log(2))
語句的掛載,當(dāng)執(zhí)行微任務(wù)隊(duì)列中的任務(wù)時,首先會執(zhí)行第一個掛載到Promise
的任務(wù),此時這個任務(wù)是運(yùn)行執(zhí)行隊(duì)列,這個隊(duì)列中有兩個方法,首先會運(yùn)行flushSchedulerQueue
方法去觸發(fā)組件的DOM
渲染操作,然后再執(zhí)行console.log(3)
,然后執(zhí)行第二個微隊(duì)列的任務(wù)也就是() => console.log(2)
,此時微任務(wù)隊(duì)列清空,然后再去宏任務(wù)隊(duì)列執(zhí)行console.log(1)
。
接下來對于沒有數(shù)據(jù)更新的updateMsgTest
按鈕觸發(fā)的方法進(jìn)行debug
,斷點(diǎn)設(shè)置在同樣的位置,此時沒有數(shù)據(jù)更新,那么第一次觸發(fā)$nextTick
方法的是自行定義的回調(diào)函數(shù),那么此時$nextTick
方法的執(zhí)行隊(duì)列才會被掛載到Promise
對象上,很顯然在此之前自行定義的輸出2
的Promise
回調(diào)已經(jīng)被掛載,那么對于這個按鈕綁定的方法的執(zhí)行流程便是首先執(zhí)行console.log(2)
,然后執(zhí)行$nextTick
方法閉包的執(zhí)行隊(duì)列,此時執(zhí)行隊(duì)列中只有一個回調(diào)函數(shù)console.log(3)
,此時微任務(wù)隊(duì)列清空,然后再去宏任務(wù)隊(duì)列執(zhí)行console.log(1)
。
簡單來說就是誰先掛載Promise
對象的問題,在調(diào)用$nextTick
方法時就會將其閉包內(nèi)部維護(hù)的執(zhí)行隊(duì)列掛載到Promise
對象,在數(shù)據(jù)更新時Vue
內(nèi)部首先就會執(zhí)行$nextTick
方法,之后便將執(zhí)行隊(duì)列掛載到了Promise
對象上,其實(shí)在明白Js
的Event Loop
模型后,將數(shù)據(jù)更新也看做一個$nextTick
方法的調(diào)用,并且明白$nextTick
方法會一次性執(zhí)行所有推入的回調(diào),就可以明白其執(zhí)行順序的問題了,下面是一個關(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ì)列的方法")); // 注釋 / 取消注釋 來查看效果 setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) nextTick(() => { console.log(3) }) })();
以上就是Vue采用異步渲染的原理分析的詳細(xì)內(nèi)容,更多關(guān)于Vue 異步渲染的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue中封裝axios并實(shí)現(xiàn)api接口的統(tǒng)一管理
這篇文章主要介紹了vue中封裝axios并實(shí)現(xiàn)api接口的統(tǒng)一管理的方法,幫助大家更好的理解和使用vue,感興趣的朋友可以了解下2020-12-12vue使用axios實(shí)現(xiàn)excel文件下載的功能
這篇文章主要介紹了vue中使用axios實(shí)現(xiàn)excel文件下載的功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07vue移動端使用canvas簽名的實(shí)現(xiàn)
這篇文章主要介紹了vue移動端使用canvas簽名的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01Vue如何實(shí)現(xiàn)iframe的上一步、下一步操作
這篇文章主要介紹了Vue如何實(shí)現(xiàn)iframe的上一步、下一步操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06