使用場景:
在我們開發專案的時候,總會碰到一些場景:當我們使用vue操作更新dom後,需要對新的dom做一些操作時,但是這個時候,我們往往會獲取不到跟新後的DOM.因為這個時候,dom還沒有重新渲染,所以我們就要使用vm.$nextTick方法。
用法:
nextTick接受一個回撥函式作為引數,它的作用將回撥延遲到下次DOM跟新週期之後執行。
methods:{
example:function(){
//修改資料
this.message='changed'
//此時dom還沒有跟新,不能獲取新的資料
this.$nextTick(function(){
//dom現在跟新了
//可以獲取新的dom資料,執行操作
this.doSomeThing()
})
}
}
複製程式碼
小思考:
在用法中,我們發現,什麼是下次DOM更新週期之後執行,具體是什麼時候,所以,我們要明白什麼是DOM更新週期。 在Vue當中,當檢視狀態發生變化時,watcher會得到通知,然後觸發虛擬DOM的渲染流程,渲染這個操作不是同步的,是非同步。Vue中有一個佇列,每當渲染時,會將watcher推送這個佇列,在下一次事件迴圈中,讓watcher觸發渲染流程。
為什麼Vue使用非同步更新佇列?
簡單來說,就是提升效能,提升效率。 我們知道Vue2.0使用虛擬dom來進行渲染,變化偵測的通知只傳送到元件上,元件上的任意一個變化都會通知到一個watcher上,然後虛擬DOM會對整個元件進行比對(diff演算法,以後有時間我會詳細研究一下),然後更新DOM.如果在同一輪事件迴圈中有兩個資料發生變化了,那麼元件的watcher會收到兩次通知,從而進行兩次渲染(同步跟新也是兩次渲染),事實上我們並不需要渲染這麼多次,只需要等所有狀態都修改完畢後,一次性將整個元件的DOM渲染到最新即可。
如何解決一次事件迴圈元件多次狀態改變只需要一次渲染更新?
其實很簡單,就是將收到的watcher例項加入佇列裡快取起來,並且再新增佇列之前檢查這個佇列是否已存在相同watcher。不存在時,才將watcher例項新增到佇列中。然後再下一次事件迴圈中,Vue會讓這個佇列中的watcher觸發渲染並清空佇列。這樣就保證一次事件迴圈元件多次狀態改變只需要一次渲染更新。
什麼是事件迴圈?
我們知道js是一門單執行緒非阻塞的指令碼語言,意思是執行js程式碼時,只有一個主執行緒來處理所有任務。非阻塞是指當程式碼需要處理非同步任務時,主執行緒會掛起(pending),當非同步任務處理完畢,主執行緒根據一定的規則去執行回撥。事實上,當任務執行完畢,js會將這個事件加入一個佇列(事件佇列)。被放入佇列中的事件不會立刻執行其回撥,而是當前執行棧中所有任務執行完畢後,主執行緒會去查詢事件佇列中是否有任務。
非同步任務有兩種型別,微任務和巨集任務。不同型別的任務會被分配到不同的任務佇列中。
執行棧中所有任務執行完畢後,主執行緒會去查詢事件佇列中是否有任務,如果存在,依次執行所有佇列中的回撥,只到為空。然後再去巨集任務佇列中取出一個事件,把對應的回撥加入當前執行棧,當前執行棧中所有任務都執行完畢,檢查微任務佇列是否有事件。無線迴圈此過程,叫做事件迴圈。
常見的微任務
- Promise.then
- Object.observe
- MutationObserver
常見的巨集任務
- setTimeout
- setInterval
- setImmediate
- UI互動事件
在我們使用vm.$nextTick中獲取跟新後DOM時,一定要在更改資料的後面使用nextTick註冊回撥。
methods:{
example:function(){
//修改資料
this.message='changed'
//此時dom還沒有跟新,不能獲取新的資料
this.$nextTick(function(){
//dom現在跟新了
//可以獲取新的dom資料,執行操作
this.doSomeThing()
})
}
}
複製程式碼
如果是先使用nextTick註冊回撥,然後修改資料,在微任務佇列中先執行使用nextTick註冊的回撥,然後才執行跟新DOM的回撥,所以回撥中得不到新的DOM,因為還沒有更新。
methods:{
example:function(){
//此時dom還沒有跟新,不能獲取新的資料
this.$nextTick(function(){
//dom沒有跟新,不能獲取新的dom
this.doSomeThing()
})
//修改資料
this.message='changed'
}
}
複製程式碼
我們知道,新增微任務佇列中的任務執行機制要高於巨集任務的執行機制(下面程式碼必須理解)
methods:{
example:function(){
//先試用setTimeout向巨集任務中註冊回撥
setTimeout(()=>{
//現在DOM已經跟新了,可以獲取最新DOM
})
//然後修改資料
this.message='changed'
}
}
複製程式碼
setTimeout屬於巨集任務,使用它註冊回撥會加入巨集任務中,巨集任務執行要比微任務晚,所以即便是先註冊,也是先跟新DOM後執行setTineout中設定回撥。
理解nextTick的作用後,我們以下來介紹實現原理
實現原理剖析:
由於nextTick會將回撥新增到任務佇列中延遲執行,所以在回撥執行之前,如果反覆使用nextTick,Vue並不會將回撥新增到任務佇列中,只會新增一個任務。Vue內部有一個列表來儲存nextTick引數中提供的回撥,當任務觸發時,以此執行列表裡的所有回撥並清空列表,其程式碼如下(簡易版):
const callbacks=[]
let pending=false
function flushCallBacks(){
pending=false
const copies=callbacks.slice(0)
callbacks.length=0
for(let i=0;i<copies.length;i++){
copies[i]()
}
}
let microTimeFun
const p=Promise.resolve()
microTimeFun=()=>{
p.then(flushCallBacks)
}
export function nextTick(cb,ctx){
callbacks.push(()=>{
if(cb){
cb.call(ctx)
}
})
if(!pending){
pending=true
microTimeFun()
}
}
複製程式碼
理解相關變數:
- callbacks:用來儲存使用者註冊的回撥函式(獲得了更新後DOM所進行的操作)
- pending:用來標記是否向任務佇列新增任務,pending為false,表示任務佇列沒有nextTIck任務,需要新增nextTick任務,當新增一個nextTick任務時,pending為ture,在回撥執行之前還有nextTick時,並不會重複新增任務到任務佇列,當回撥函式開始執行時,pending為flase,進行新的一輪事件迴圈。
- flushCallbacks:就是我們所說的被註冊在任務佇列中的任務,當這個函式執行,callbacks中所有函式依次執行,然後清空callbacks,並重置pending為false,所以說,一輪事件迴圈中,flushCallbacks只會執行一次。
- microTimerFunc:它的作用就是使用Promise.then將flushCallbacks新增到微任務佇列中。
下圖給出nextTick內部註冊流程和執行流程。
官方文件裡面還有這麼一句話,如果沒有提供回撥且支援Promise的環境下,則返回一個Promise。也就是說。可以這樣使用nextTickthis.$nextTick().then(function(){
//dom跟新了
})
複製程式碼
要實現這個功能,只需要在nextTIck中判斷,如果沒有提供回撥且當前支援Promise,那麼返回Promise,並且在callbacks中新增一個函式,當這個函式執行時,執行Promise的resolve,即可,程式碼如下
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
cb.call(ctx);
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
複製程式碼
nextTick原始碼檢視
到此,nextTick原理基本上已經講完了。那我們現在可以看看真正vue中關於nextTick中的原始碼,大概我們都能理解的過來了,原始碼如下。
var timerFunc;
// 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 next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
// 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); }
};
isUsingMicroTask = true;
} else if (!isIE && 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, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
複製程式碼
總結
這篇文章大概花了兩天時間才寫出來的,充分的參考了<深入淺出vue.js>這本書,充分了理解書上關於vm.$nextTick中的每一句話,同時也對js中的事件迴圈有了進一步認識,對js執行機制也進一步加深。作為前端小白,不想只侷限於呼叫各種API,更要知道其原理,每天進步一小步。希望大家能多多與我討論交流。