vue nextTick原理
前面談到了vue2.x的響應(yīng)式原理,vue.js在視圖更新采用的是異步更新策略棺棵,我們來看看它是怎么做到的楼咳。
/** ? */
for(let i = 0; i < 100; i++) {
this.count++;
}
/** 烛恤? */
在dom更新后執(zhí)行一些操作
this.$nextTick(fn)
先拋出兩個問題:
- for循環(huán)更新count數(shù)值母怜,dom會被更新100次嗎?
- nextTick是如何做到監(jiān)聽dom更新完畢的缚柏?
異步更新涉及到j(luò)s的運行機制苹熏,詳細的可看這里
【event loop機制】
這篇文章呢我們主要從源碼角度來分析nextTick的原理實現(xiàn)。
這是我們響應(yīng)式里面的watcher類
<!--觀察者Watcher類-->
class Watcher {
constructor () {
Dep.target = this // new Watcher的時候把觀察者存放到Dep.target里面
}
update () {
queueWatcher(this) // 異步更新策略
}
run () {
// dom在這里執(zhí)行真正的更新
}
}
watcher對象在進行更新執(zhí)行update币喧,內(nèi)部主要執(zhí)行了一個queueWatcher函數(shù)轨域,將watcher對象作為this進行傳遞,所以我們便從queueWatcher這個口子開始杀餐。
queueWatcher
queueWatcher函數(shù)在scheduler文件里面
/** queueWatcher函數(shù)*/
let has = {};
let queue = [];
let waiting = false;
function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 防止queue隊列wachter對象重復(fù)
if (has[id] == null) {
has[id] = true
queue.push(watcher)
// 傳遞本次的更新任務(wù)
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
/** flushSchedulerQueue函數(shù) */
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
// 執(zhí)行更新
watcher.run();
}
// 更新完畢恢復(fù)標志位
waiting = false;
}
- queue里面存放著我們本次要更新的watcher對象干发,queueWatcher函數(shù)做了一個判重操作,相同的watcher對象只會被加入到queue隊列一次史翘。
- flushSchedulerQueue函數(shù)依次調(diào)用了wacther對象的run方法執(zhí)行更新铐然。并作為回調(diào)傳遞給了nextTick函數(shù)。
- waiting這個標記位代表我們是否已經(jīng)向nextTick函數(shù)傳遞了更新任務(wù)恶座,nextTick會在當前task結(jié)束后再去處理傳入的回掉,只需要傳遞一次沥阳,更新完畢再重置這個標志位跨琳。
next-tick
let callbacks = [];
let pending = false;
let timerFunc;
/**----- nextTick -----*/
function nextTick (cb) {
// 把傳進來的回調(diào)函數(shù)放到callbacks隊列里
callbacks.push(cb);
// pending代表一個等待狀態(tài) 等這個tick執(zhí)行
if (!pending) {
pending = true
timerFunc()
}
// 如果沒傳遞回調(diào) 提供一個Promise化的調(diào)用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
/**----- timerFunc ----*/
// 1、優(yōu)先考慮Promise實現(xiàn)
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// 2桐罕、降級到MutationObserver實現(xiàn)
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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 3脉让、降級到setImmediate實現(xiàn)
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 4、如果以上都不支持就用setTimeout來兜底了
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
function flushCallbacks () {
// 將callbacks中的cb依次執(zhí)行
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
-
傳進來的回調(diào)函數(shù)會被保存到callbacks隊列里面功炮,這里使用callbacks 而沒有在nextTick中直接執(zhí)行回調(diào)函數(shù)溅潜,是因為這樣可以保證在同一個tick 內(nèi)多次執(zhí)行nextTick,在一個tick里面完成渲染薪伏,不會開啟多個異步任務(wù)滚澜。
// 舉個栗子?? // 假如我們直接在nexttick里面直接執(zhí)行回調(diào) function nextTick (cb) { setTimeout(cb) } nextTick(cb1) nextTick(cb2) 這種情況下就會開啟兩個異步任務(wù),也就是兩次事件循環(huán)嫁怀,造成了頁面不必要的渲染
timerFunc是實現(xiàn)的核心设捐,它會優(yōu)先使用Promise等microtask借浊,保證在同一個事件循環(huán)里面執(zhí)行,這樣頁面只需要渲染一次萝招。實在不行的話用setTimeout來兜底蚂斤,雖然會造成二次渲染,但這也是最差的情況槐沼。vue在這里用了降級處理的策略曙蒸。
$nextTick
最后再把nexttick函數(shù)掛到Vue原型上就OK了
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
}
小結(jié)
vue異步更新,本質(zhì)上是js事件機制的一種運用岗钩,優(yōu)先考慮了具有高優(yōu)先級的microtask纽窟,為了兼容,又做了降級策略凹嘲。
現(xiàn)在再回頭看開頭的那兩個問題
-
for循環(huán)更新count數(shù)值师倔,dom會被更新100次嗎?
不會周蹭,因為queueWatcher函數(shù)做了過濾趋艘,相同的watcher對象不會被重復(fù)添加。
-
nextTick是如何做到監(jiān)聽dom更新完畢的凶朗?
vue用異步隊列的方式來控制DOM更新和nextTick回調(diào)先后執(zhí)行瓷胧,保證了能在dom更新后在執(zhí)行回調(diào)。