1. 程式人生 > 其它 >vue $nextTick 原理詳解

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的執行機制

  1. 所有同步任務都在主執行緒上執行,形成一個[執行棧]

  2. 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。

  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行(執行順序,首先執行完當前佇列的微任務,再去執行當前佇列的巨集任務)。

  4. 主執行緒不斷重複上面的第三步。

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示例observer,傳參回撥為 flushCallbacks

** 建立文字節點textNode,並通過observer監聽 textNode 的內容變化

** 設定timerFunc函式,以觸發textNode 的變化,觸發 flushCallbacks

  • 判斷是否支援setImmediate;

  • 以上全部不支援,則通過setTimeout(fn, 0)模擬非同步延遲呼叫。

Vue.prototype.$nextTick=function(fn){
returnnextTick(fn,this)
};

參考文獻