vue $nextTick 原理詳解
一.nextTick定義
二.為什麼使用 nextTick
Vue 在更新 DOM 時是非同步執行的。
只要偵聽到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料變更。如果同一個 watcher 被多次觸發,只會被推入到佇列中一次。在下一個的事件迴圈“tick”中,Vue 重新整理佇列並執行實際 (已去重的) 工作。
在事件環境中的資料變化完成,在進行渲染[檢視更新],可以避免DOM的頻繁變動,從而避免了因此帶來的瀏覽器卡頓,大幅度提升效能.
三.nextTick的理解和應用場景
-
在鉤子函式created()裡面想要獲取dom的內容或者操作dom
-
在檢視data更新後,基於新的檢視操作該dom
在下次 DOM 更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的 DOM
//改變資料
vm.message = 'changed'
//想要立即使用更新後的DOM。這樣不行,因為設定message後DOM還沒有更新
console.log(vm.$el.textContent)
// 並不會得到'changed' //這樣可以,nextTick裡面的程式碼會在DOM更新後執行
Vue.nextTick(function(){
console.log(vm.$el.textContent)
//可以得到'changed'
})
四.事件佇列(非阻塞,單執行緒)
1.JavaScript的執行機制
-
所有同步任務都在主執行緒上執行,形成一個[執行棧]
-
主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
-
一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行(執行順序,首先執行完當前佇列的微任務,再去執行當前佇列的巨集任務)。
-
主執行緒不斷重複上面的第三步。
2.概括
呼叫棧中的同步任務都執行完畢,棧內被清空了,就代表主執行緒空閒了,這個時候就會去任務佇列中按照順序讀取一個任務放入到棧中執行。每次棧內被清空,都會去讀取任務佇列有沒有任務,有就讀取執行,一直迴圈讀取-執行的操作。
3.MacroTask(巨集任務)/ MicroTask(微任務)
MacroTask(巨集任務):script、setTimeout、setInterval、setImmediate(瀏覽器暫時不支援,只有IE10支援,具體可見MDN)。
MicroTask(微任務):Process.nextTick(Node獨有)、Promise、Object.observe、MutationObserver(具體使用方式檢視這裡)
4.示例練習
*** 判斷其輸出
setTimeout(()=>{
console.log("setTimeout1");
Promise.resolve().then(data => { console.log(222); });
});
setTimeout(()=>{ console.log("setTimeout2"); });
Promise.resolve().then(data=>{ console.log(111); })
*** 判斷其輸出
console.log('script start');
setTimeout(function () {
console.log('setTimeout---0');
}, 0);
setTimeout(function () {
console.log('setTimeout---200');
setTimeout(function () {
console.log('inner-setTimeout---0');
});
Promise.resolve().then(function () {
console.log('promise5');
});
}, 200);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('script end');
五.原始碼解析
/*@flow*/
/*globalsMutationObserver*/
import{noop}from'shared/util'
import{handleError}from'./error'
import{isIE,isIOS,isNative}from'./env'
/** 是否是微任務 */
exportletisUsingMicroTask=false
/** 用來儲存所有需要執行的回撥函式 */
constcallbacks=[]
/*
* 表示狀態,判斷是否有正在執行的回撥函式。
* 如果程式碼中 timerFunc 函式被推送到任務佇列中去則不需要重複推送。
*/
letpending=false
/** 用來執行callbacks裡面儲存的所有回撥函式 */
functionflushCallbacks(){
pending=false
constcopies=callbacks.slice(0)
callbacks.length=0
for(leti=0;i<copies.length;i++){
copies[i]()
}
}
/** 儲存需要被執行的函式。 */
lettimerFunc
/**判斷是否支出 Promise 原生呼叫 */
if(typeofPromise!=='undefined'&&isNative(Promise)){
constp=Promise.resolve()
timerFunc=()=>{
p.then(flushCallbacks)
if(isIOS)setTimeout(noop)
}
isUsingMicroTask=true
/** 判斷是否支援 MutationObserver */
}elseif(!isIE&&typeofMutationObserver!=='undefined'&&(
isNative(MutationObserver)||
//PhantomJSandiOS7.x
MutationObserver.toString()==='[objectMutationObserverConstructor]'
)){
letcounter=1
constobserver=newMutationObserver(flushCallbacks)
consttextNode=document.createTextNode(String(counter))
observer.observe(textNode,{
characterData:true
})
timerFunc=()=>{
counter=(counter+1)%2
textNode.data=String(counter)
}
isUsingMicroTask=true
/** 判斷是否支援 setImmediate*/
}elseif(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){
//FallbacktosetImmediate.
//Technicallyitleveragesthe(macro)taskqueue,
//butitisstillabetterchoicethansetTimeout.
timerFunc=()=>{
setImmediate(flushCallbacks)
}
}else{
//FallbacktosetTimeout.
timerFunc=()=>{
setTimeout(flushCallbacks,0)
}
}
/**
* @params cb 回撥
* @params ctx 執行上下文
*/
exportfunctionnextTick(cb?:Function,ctx?:Object){
let_resolve
callbacks.push(()=>{
if(cb){
try{
cb.call(ctx)
}catch(e){
handleError(e,ctx,'nextTick')
}
}elseif(_resolve){
_resolve(ctx)
}
})
if(!pending){
pending=true
timerFunc()
}
//$flow-disable-line
if(!cb&&typeofPromise!=='undefined'){
returnnewPromise(resolve=>{
_resolve=resolve
})
}
}
以上程式碼表明在vue中處理非同步延遲呼叫的方式主要如下:
-
判斷是否支援原生Promise;
-
判斷是否支援原生MutationObserver,若支援:
** 建立MutationObserver示例observer,傳參回撥為 flushCallbacks
** 建立文字節點textNode,並通過observer監聽 textNode 的內容變化
** 設定timerFunc函式,以觸發textNode 的變化,觸發 flushCallbacks
-
判斷是否支援setImmediate;
-
以上全部不支援,則通過setTimeout(fn, 0)模擬非同步延遲呼叫。
Vue.prototype.$nextTick=function(fn){
returnnextTick(fn,this)
};
參考文獻