一.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 */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
/** 是否是微任務 */
export let isUsingMicroTask = false
/** 用來存儲所有需要執行的回調函數 */
const callbacks = []
/*
* 表示狀態,判斷是否有正在執行的回調函數。
* 如果代碼中 timerFunc 函數被推送到任務隊列中去則不需要重復推送。
*/
let pending = false
/** 用來執行callbacks里面存儲的所有回調函數 */
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
/** 保存需要被執行的函數。 */
let timerFunc
/**判斷是否支出 Promise 原生調用 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
/** 判斷是否支持 MutationObserver */
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
/** 判斷是否支持 setImmediate */
} 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 = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
/**
* @params cb 回調
* @params ctx 執行上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
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(resolve => {
_resolve = resolve
})
}
}
以上代碼表明在vue中處理異步延遲調用的方式主要如下:
-
判斷是否支持原生Promise;
-
判斷是否支持原生MutationObserver,若支持:
** 創建MutationObserver示例observer ,傳參回調為 flushCallbacks
** 創建文本節點textNode,並通過observer 監聽 textNode 的內容變化
** 設置timerFunc函數,以觸發textNode 的變化,觸發 flushCallbacks
-
判斷是否支持setImmediate;
-
以上全部不支持,則通過setTimeout(fn, 0)模擬異步延遲調用。
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
};
參考文獻
