在咱們開發項目的時候,總會碰到一些場景:當咱們使用vue操做更新dom後,須要對新的dom作一些操做時,可是這個時候,咱們每每會獲取不到跟新後的DOM.由於這個時候,dom尚未從新渲染,因此咱們就要使用vm.$nextTick方法。前端
nextTick接受一個回調函數做爲參數,它的做用將回調延遲到下次DOM跟新週期以後執行。vue
methods:{
example:function(){
//修改數據
this.message='changed'
//此時dom尚未跟新,不能獲取新的數據
this.$nextTick(function(){
//dom如今跟新了
//能夠獲取新的dom數據,執行操做
this.doSomeThing()
})
}
}
複製代碼
在用法中,咱們發現,什麼是下次DOM更新週期以後執行,具體是何時,因此,咱們要明白什麼是DOM更新週期。 在Vue當中,當視圖狀態發生變化時,watcher會獲得通知,而後觸發虛擬DOM的渲染流程,渲染這個操做不是同步的,是異步。Vue中有一個隊列,每當渲染時,會將watcher推送這個隊列,在下一次事件循環中,讓watcher觸發渲染流程。算法
簡單來講,就是提高性能,提高效率。 咱們知道Vue2.0使用虛擬dom來進行渲染,變化偵測的通知只發送到組件上,組件上的任意一個變化都會通知到一個watcher上,而後虛擬DOM會對整個組件進行比對(diff算法,之後有時間我會詳細研究一下),而後更新DOM.若是在同一輪事件循環中有兩個數據發生變化了,那麼組件的watcher會收到兩次通知,從而進行兩次渲染(同步跟新也是兩次渲染),事實上咱們並不須要渲染這麼屢次,只須要等全部狀態都修改完畢後,一次性將整個組件的DOM渲染到最新便可。緩存
其實很簡單,就是將收到的watcher實例加入隊列裏緩存起來,而且再添加隊列以前檢查這個隊列是否已存在相同watcher。不存在時,纔將watcher實例添加到隊列中。而後再下一次事件循環中,Vue會讓這個隊列中的watcher觸發渲染並清空隊列。這樣就保證一次事件循環組件屢次狀態改變只須要一次渲染更新。bash
咱們知道js是一門單線程非阻塞的腳本語言,意思是執行js代碼時,只有一個主線程來處理全部任務。非阻塞是指當代碼須要處理異步任務時,主線程會掛起(pending),當異步任務處理完畢,主線程根據必定的規則去執行回調。事實上,當任務執行完畢,js會將這個事件加入一個隊列(事件隊列)。被放入隊列中的事件不會馬上執行其回調,而是當前執行棧中全部任務執行完畢後,主線程會去查找事件隊列中是否有任務。
異步任務有兩種類型,微任務和宏任務。不一樣類型的任務會被分配到不一樣的任務隊列中。
執行棧中全部任務執行完畢後,主線程會去查找事件隊列中是否有任務,若是存在,依次執行全部隊列中的回調,只到爲空。而後再去宏任務隊列中取出一個事件,把對應的回調加入當前執行棧,當前執行棧中全部任務都執行完畢,檢查微任務隊列是否有事件。無線循環此過程,叫作事件循環。dom
在咱們使用vm.$nextTick中獲取跟新後DOM時,必定要在更改數據的後面使用nextTick註冊回調。異步
methods:{
example:function(){
//修改數據
this.message='changed'
//此時dom尚未跟新,不能獲取新的數據
this.$nextTick(function(){
//dom如今跟新了
//能夠獲取新的dom數據,執行操做
this.doSomeThing()
})
}
}
複製代碼
若是是先使用nextTick註冊回調,而後修改數據,在微任務隊列中先執行使用nextTick註冊的回調,而後才執行跟新DOM的回調,因此回調中得不到新的DOM,由於尚未更新。ide
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,Vue並不會將回調添加到任務隊列中,只會添加一個任務。Vue內部有一個列表來存儲nextTick參數中提供的回調,當任務觸發時,以此執行列表裏的全部回調並清空列表,其代碼以下(簡易版):oop
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()
}
}
複製代碼
理解相關變量:
this.$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原理基本上已經講完了。那咱們如今能夠看看真正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,更要知道其原理,天天進步一小步。但願你們能多多與我討論交流。