您好,登錄后才能下訂單哦!
這篇文章主要介紹“vue頁面渲染是異步的嗎”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“vue頁面渲染是異步的嗎”文章能幫助大家解決問題。
vue頁面渲染是異步的。vue采用的是異步渲染,這樣可以提升性能;如果不采用異步更新,在每次更新數據都會對當前組件進行重新渲染,為了性能考慮,Vue會在本輪數據更新后,再去異步更新視圖。
vue頁面渲染是異步的。
Vue
在更新DOM
時是異步執行的,只要偵聽到數據變化,Vue
將開啟一個隊列,并緩沖在同一事件循環中發生的所有數據變更,如果同一個watcher
被多次觸發,只會被推入到隊列中一次,這種在緩沖時去除重復數據對于避免不必要的計算和DOM
操作是非常重要的,然后,在下一個的事件循環tick
中,Vue
刷新隊列并執行實際(已去重的)工作,Vue
在內部對異步隊列嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,如果執行環境不支持,則會采用setTimeout(fn, 0)
代替。
對于Vue
為何采用異步渲染,簡單來說就是為了提升性能,因為不采用異步更新,在每次更新數據都會對當前組件進行重新渲染,為了性能考慮,Vue
會在本輪數據更新后,再去異步更新視圖,舉個例子,讓我們在一個方法內重復更新一個值。
this.msg = 1;
this.msg = 2;
this.msg = 3;
事實上,我們真正想要的其實只是最后一次更新而已,也就是說前三次DOM
更新都是可以省略的,我們只需要等所有狀態都修改好了之后再進行渲染就可以減少一些性能損耗。
對于渲染方面的問題是很明確的,最終只渲染一次肯定比修改之后即渲染所耗費的性能少,在這里我們還需要考慮一下異步更新隊列的相關問題,假設我們現在是進行了相關處理使得每次更新數據只進行一次真實DOM
渲染,來讓我們考慮異步更新隊列的性能優化。
假設這里是同步更新隊列,this.msg=1
,大致會發生這些事: msg
值更新 ->
觸發setter
->
觸發Watcher
的update
->
重新調用 render
->
生成新的vdom -> dom-diff -> dom
更新,這里的dom
更新并不是渲染(即布局、繪制、合成等一系列步驟),而是更新內存中的DOM
樹結構,之后再運行this.msg=2
,再重復上述步驟,之后的第3
次更新同樣會觸發相同的流程,等開始渲染的時候,最新的DOM
樹中確實只會存在更新完成3
,從這里來看,前2
次對msg
的操作以及Vue
內部對它的處理都是無用的操作,可以進行優化處理。
如果是異步更新隊列,會是下面的情況,運行this.msg=1
,并不是立即進行上面的流程,而是將對msg
有依賴的Watcher
都保存在隊列中,該隊列可能這樣[Watcher1, Watcher2...]
,當運行this.msg=2
后,同樣是將對msg
有依賴的Watcher
保存到隊列中,Vue
內部會做去重判斷,這次操作后,可以認為隊列數據沒有發生變化,第3
次更新也是上面的過程,當然,你不可能只對msg
有操作,你可能對該組件中的另一個屬性也有操作,比如this.otherMsg=othermessage
,同樣會把對otherMsg
有依賴的Watcher
添加到異步更新隊列中,因為有重復判斷操作,這個Watcher
也只會在隊列中存在一次,本次異步任務執行結束后,會進入下一個任務執行流程,其實就是遍歷異步更新隊列中的每一個Watcher
,觸發其update
,然后進行重新調用render
->
new vdom
->
dom-diff
->
dom
更新等流程,但是這種方式和同步更新隊列相比,不管操作多少次msg
, Vue
在內部只會進行一次重新調用真實更新流程,所以,對于異步更新隊列不是節省了渲染成本,而是節省了Vue
內部計算及DOM
樹操作的成本,不管采用哪種方式,渲染確實只有一次。
此外,組件內部實際使用VirtualDOM
進行渲染,也就是說,組件內部其實是不關心哪個狀態發生了變化,它只需要計算一次就可以得知哪些節點需要更新,也就是說,如果更改了N
個狀態,其實只需要發送一個信號就可以將DOM
更新到最新,如果我們更新多個值。
this.msg = 1;
this.age = 2;
this.name = 3;
此處我們分三次修改了三種狀態,但其實Vue
只會渲染一次,因為VIrtualDOM
只需要一次就可以將整個組件的DOM
更新到最新,它根本不會關心這個更新的信號到底是從哪個具體的狀態發出來的。
而為了達到這個目的,我們需要將渲染操作推遲到所有的狀態都修改完成,為了做到這一點只需要將渲染操作推遲到本輪事件循環的最后或者下一輪事件循環,也就是說,只需要在本輪事件循環的最后,等前面更新狀態的語句都執行完之后,執行一次渲染操作,它就可以無視前面各種更新狀態的語法,無論前面寫了多少條更新狀態的語句,只在最后渲染一次就可以了。
將渲染推遲到本輪事件循環的最后執行渲染的時機會比推遲到下一輪快很多,所以Vue
優先將渲染操作推遲到本輪事件循環的最后,如果執行環境不支持會降級到下一輪,Vue
的變化偵測機制(setter
)決定了它必然會在每次狀態發生變化時都會發出渲染的信號,但Vue
會在收到信號之后檢查隊列中是否已經存在這個任務,保證隊列中不會有重復,如果隊列中不存在則將渲染操作添加到隊列中,之后通過異步的方式延遲執行隊列中的所有渲染的操作并清空隊列,當同一輪事件循環中反復修改狀態時,并不會反復向隊列中添加相同的渲染操作,所以我們在使用Vue
時,修改狀態后更新DOM
都是異步的。
當數據變化后會調用notify
方法,將watcher
遍歷,調用update
方法通知watcher
進行更新,這時候watcher
并不會立即去執行,在update
中會調用queueWatcher
方法將watcher
放到了一個隊列里,在queueWatcher
會根據watcher
的進行去重,若多個屬性依賴一個watcher
,則如果隊列中沒有該watcher
就會將該watcher
添加到隊列中,然后便會在$nextTick
方法的執行隊列中加入一個flushSchedulerQueue
方法(這個方法將會觸發在緩沖隊列的所有回調的執行),然后將$nextTick
方法的回調加入$nextTick
方法中維護的執行隊列,flushSchedulerQueue
中開始會觸發一個before
的方法,其實就是beforeUpdate
,然后watcher.run
()才開始真正執行watcher
,執行完頁面就渲染完成,更新完成后會調用updated
鉤子。
在上文中談到了對于Vue
為何采用異步渲染,假如此時我們有一個需求,需要在頁面渲染完成后取得頁面的DOM
元素,而由于渲染是異步的,我們不能直接在定義的方法中同步取得這個值的,于是就有了vm.$nextTick
方法,Vue
中$nextTick
方法將回調延遲到下次DOM
更新循環之后執行,也就是在下次DOM
更新循環結束之后執行延遲回調,在修改數據之后立即使用這個方法,能夠獲取更新后的DOM
。簡單來說就是當數據更新時,在DOM
中渲染完成后,執行回調函數。
通過一個簡單的例子來演示$nextTick
方法的作用,首先需要知道Vue
在更新DOM
時是異步執行的,也就是說在更新數據時其不會阻塞代碼的執行,直到執行棧中代碼執行結束之后,才開始執行異步任務隊列的代碼,所以在數據更新時,組件不會立即渲染,此時在獲取到DOM
結構后取得的值依然是舊的值,而在$nextTick
方法中設定的回調函數會在組件渲染完成之后執行,取得DOM
結構后取得的值便是新的值。
<!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>
官方文檔中說明,Vue
在更新DOM
時是異步執行的,只要偵聽到數據變化,Vue
將開啟一個隊列,并緩沖在同一事件循環中發生的所有數據變更,如果同一個watcher
被多次觸發,只會被推入到隊列中一次。這種在緩沖時去除重復數據對于避免不必要的計算和DOM
操作是非常重要的。然后,在下一個的事件循環tick
中,Vue
刷新隊列并執行實際工作。Vue
在內部對異步隊列嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,如果執行環境不支持,則會采用 setTimeout(fn, 0)
代替。Js
是單線程的,其引入了同步阻塞與異步非阻塞的執行模式,在Js
異步模式中維護了一個Event Loop
,Event Loop
是一個執行模型,在不同的地方有不同的實現,瀏覽器和NodeJS
基于不同的技術實現了各自的Event Loop
。瀏覽器的Event Loop
是在HTML5
的規范中明確定義,NodeJS
的Event Loop
是基于libuv
實現的。
在瀏覽器中的Event Loop
由執行棧Execution Stack
、后臺線程Background Threads
、宏隊列Macrotask Queue
、微隊列Microtask Queue
組成。
執行棧就是在主線程執行同步任務的數據結構,函數調用形成了一個由若干幀組成的棧。
后臺線程就是瀏覽器實現對于setTimeout
、setInterval
、XMLHttpRequest
等等的執行線程。
宏隊列,一些異步任務的回調會依次進入宏隊列,等待后續被調用,包括setTimeout
、setInterval
、setImmediate(Node)
、requestAnimationFrame
、UI rendering
、I/O
等操作。
微隊列,另一些異步任務的回調會依次進入微隊列,等待后續調用,包括Promise
、process.nextTick(Node)
、Object.observe
、MutationObserver
等操作。
當Js
執行時,進行如下流程:
首先將執行棧中代碼同步執行,將這些代碼中異步任務加入后臺線程中。
執行棧中的同步代碼執行完畢后,執行棧清空,并開始掃描微隊列。
取出微隊列隊首任務,放入執行棧中執行,此時微隊列是進行了出隊操作。
當執行棧執行完成后,繼續出隊微隊列任務并執行,直到微隊列任務全部執行完畢。
最后一個微隊列任務出隊并進入執行棧后微隊列中任務為空,當執行棧任務完成后,開始掃面微隊列為空,繼續掃描宏隊列任務,宏隊列出隊,放入執行棧中執行,執行完畢后繼續掃描微隊列為空則掃描宏隊列,出隊執行。
不斷往復...
。
// 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
*/
// 執行棧 console
// 微隊列 []
// 宏隊列 []
console.log(1); // 1
// 執行棧 setTimeout
// 微隊列 []
// 宏隊列 [setTimeout1]
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
}, 0);
// 執行棧 Promise
// 微隊列 [then1]
// 宏隊列 [setTimeout1]
new Promise((resolve, reject) => {
console.log(4); // 4 // Promise是個函數對象,此處是同步執行的 // 執行棧 Promise console
resolve();
}).then(() => {
console.log(5);
})
// 執行棧 setTimeout
// 微隊列 [then1]
// 宏隊列 [setTimeout1 setTimeout2]
setTimeout(() => {
console.log(6);
}, 0);
// 執行棧 console
// 微隊列 [then1]
// 宏隊列 [setTimeout1 setTimeout2]
console.log(7); // 7
// 執行棧 then1
// 微隊列 []
// 宏隊列 [setTimeout1 setTimeout2]
console.log(5); // 5
// 執行棧 setTimeout1
// 微隊列 [then2]
// 宏隊列 [setTimeout2]
console.log(2); // 2
Promise.resolve().then(() => {
console.log(3);
});
// 執行棧 then2
// 微隊列 []
// 宏隊列 [setTimeout2]
console.log(3); // 3
// 執行棧 setTimeout2
// 微隊列 []
// 宏隊列 []
console.log(6); // 6
在了解異步任務的執行隊列后,回到中$nextTick
方法,當用戶數據更新時,Vue
將會維護一個緩沖隊列,對于所有的更新數據將要進行的組件渲染與DOM
操作進行一定的策略處理后加入緩沖隊列,然后便會在$nextTick
方法的執行隊列中加入一個flushSchedulerQueue
方法(這個方法將會觸發在緩沖隊列的所有回調的執行),然后將$nextTick
方法的回調加入$nextTick
方法中維護的執行隊列,在異步掛載的執行隊列觸發時就會首先會首先執行flushSchedulerQueue
方法來處理DOM
渲染的任務,然后再去執行$nextTick
方法構建的任務,這樣就可以實現在$nextTick
方法中取得已渲染完成的DOM
結構。在測試的過程中發現了一個很有意思的現象,在上述例子中的加入兩個按鈕,在點擊updateMsg
按鈕的結果是3 2 1
,點擊updateMsgTest
按鈕的運行結果是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>
這里假設運行環境中Promise
對象是完全支持的,那么使用setTimeout
是宏隊列在最后執行這個是沒有異議的,但是使用$nextTick
方法以及自行定義的Promise
實例是有執行順序的問題的,雖然都是微隊列任務,但是在Vue
中具體實現的原因導致了執行順序可能會有所不同,首先直接看一下$nextTick
方法的源碼,關鍵地方添加了注釋,請注意這是Vue2.4.2
版本的源碼,在后期$nextTick
方法可能有所變更。
/**
* Defer a task to execute it asynchronously.
*/
var nextTick = (function () {
// 閉包 內部變量
var callbacks = []; // 執行隊列
var pending = false; // 標識,用以判斷在某個事件循環中是否為第一次加入,第一次加入的時候才觸發異步執行的隊列掛載
var timerFunc; // 以何種方法執行掛載異步執行隊列,這里假設Promise是完全支持的
function nextTickHandler () { // 異步掛載的執行任務,觸發時就已經正式準備開始執行異步任務了
pending = false; // 標識置false
var copies = callbacks.slice(0); // 創建副本
callbacks.length = 0; // 執行隊列置空
for (var i = 0; i < copies.length; i++) {
copies[i](); // 執行
}
}
// 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); // 掛載異步任務隊列
// 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方法真正導出的方法
var _resolve;
callbacks.push(function () { // 添加到執行隊列中 并加入異常處理
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
//判斷在當前事件循環中是否為第一次加入,若是第一次加入則置標識為true并執行timerFunc函數用以掛載執行隊列到Promise
// 這個標識在執行隊列中的任務將要執行時便置為false并創建執行隊列的副本去運行執行隊列中的任務,參見nextTickHandler函數的實現
// 在當前事件循環中置標識true并掛載,然后再次調用nextTick方法時只是將任務加入到執行隊列中,直到掛載的異步任務觸發,便置標識為false然后執行任務,再次調用nextTick方法時就是同樣的執行方式然后不斷如此往復
if (!pending) {
pending = true;
timerFunc();
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {
_resolve = resolve;
})
}
}
})();
回到剛才提出的問題上,在更新DOM
操作時會先觸發$nextTick
方法的回調,解決這個問題的關鍵在于誰先將異步任務掛載到Promise
對象上。
首先對有數據更新的updateMsg
按鈕觸發的方法進行debug
,斷點設置在Vue.js
的715
行,版本為2.4.2
,在查看調用棧以及傳入的參數時可以觀察到第一次執行$nextTick
方法的其實是由于數據更新而調用的nextTick(flushSchedulerQueue);
語句,也就是說在執行this.msg = "Update";
的時候就已經觸發了第一次的$nextTick
方法,此時在$nextTick
方法中的任務隊列會首先將flushSchedulerQueue
方法加入隊列并掛載$nextTick
方法的執行隊列到Promise
對象上,然后才是自行自定義的Promise.resolve().then(() => console.log(2))
語句的掛載,當執行微任務隊列中的任務時,首先會執行第一個掛載到Promise
的任務,此時這個任務是運行執行隊列,這個隊列中有兩個方法,首先會運行flushSchedulerQueue
方法去觸發組件的DOM
渲染操作,然后再執行console.log(3)
,然后執行第二個微隊列的任務也就是() => console.log(2)
,此時微任務隊列清空,然后再去宏任務隊列執行console.log(1)
。
接下來對于沒有數據更新的updateMsgTest
按鈕觸發的方法進行debug
,斷點設置在同樣的位置,此時沒有數據更新,那么第一次觸發$nextTick
方法的是自行定義的回調函數,那么此時$nextTick
方法的執行隊列才會被掛載到Promise
對象上,很顯然在此之前自行定義的輸出2
的Promise
回調已經被掛載,那么對于這個按鈕綁定的方法的執行流程便是首先執行console.log(2)
,然后執行$nextTick
方法閉包的執行隊列,此時執行隊列中只有一個回調函數console.log(3)
,此時微任務隊列清空,然后再去宏任務隊列執行console.log(1)
。
簡單來說就是誰先掛載Promise
對象的問題,在調用$nextTick
方法時就會將其閉包內部維護的執行隊列掛載到Promise
對象,在數據更新時Vue
內部首先就會執行$nextTick
方法,之后便將執行隊列掛載到了Promise
對象上,其實在明白Js
的Event Loop
模型后,將數據更新也看做一個$nextTick
方法的調用,并且明白$nextTick
方法會一次性執行所有推入的回調,就可以明白其執行順序的問題了,下面是一個關于$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("觸發DOM渲染隊列的方法")); // 注釋 / 取消注釋 來查看效果
setTimeout(() => console.log(1))
Promise.resolve().then(() => console.log(2))
nextTick(() => {
console.log(3)
})
})();
關于“vue頁面渲染是異步的嗎”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。