vue nextTick原理
前面谈到了vue2.x的响应式原理,vue.js在视图更新采用的是异步更新策略,我们来看看它是怎么做到的。
/** ? */
for(let i = 0; i < 100; i++) {
this.count++;
}
/** ? */
在dom更新后执行一些操作
this.$nextTick(fn)
先抛出两个问题:
- for循环更新count数值,dom会被更新100次吗?
- nextTick是如何做到监听dom更新完毕的?
异步更新涉及到js的运行机制,详细的可看这里
【event loop机制】
这篇文章呢我们主要从源码角度来分析nextTick的原理实现。
这是我们响应式里面的watcher类
<!--观察者Watcher类-->
class Watcher {
constructor () {
Dep.target = this // new Watcher的时候把观察者存放到Dep.target里面
}
update () {
queueWatcher(this) // 异步更新策略
}
run () {
// dom在这里执行真正的更新
}
}
watcher对象在进行更新执行update,内部主要执行了一个queueWatcher函数,将watcher对象作为this进行传递,所以我们便从queueWatcher这个口子开始。
queueWatcher
queueWatcher函数在scheduler文件里面
/** queueWatcher函数*/
let has = {};
let queue = [];
let waiting = false;
function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 防止queue队列wachter对象重复
if (has[id] == null) {
has[id] = true
queue.push(watcher)
// 传递本次的更新任务
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
/** flushSchedulerQueue函数 */
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
// 执行更新
watcher.run();
}
// 更新完毕恢复标志位
waiting = false;
}
- queue里面存放着我们本次要更新的watcher对象,queueWatcher函数做了一个判重操作,相同的watcher对象只会被加入到queue队列一次。
- flushSchedulerQueue函数依次调用了wacther对象的run方法执行更新。并作为回调传递给了nextTick函数。
- waiting这个标记位代表我们是否已经向nextTick函数传递了更新任务,nextTick会在当前task结束后再去处理传入的回掉,只需要传递一次,更新完毕再重置这个标志位。
next-tick
let callbacks = [];
let pending = false;
let timerFunc;
/**----- nextTick -----*/
function nextTick (cb) {
// 把传进来的回调函数放到callbacks队列里
callbacks.push(cb);
// pending代表一个等待状态 等这个tick执行
if (!pending) {
pending = true
timerFunc()
}
// 如果没传递回调 提供一个Promise化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
/**----- timerFunc ----*/
// 1、优先考虑Promise实现
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实现
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实现
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 4、如果以上都不支持就用setTimeout来兜底了
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
function flushCallbacks () {
// 将callbacks中的cb依次执行
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
-
传进来的回调函数会被保存到callbacks队列里面,这里使用callbacks 而没有在nextTick中直接执行回调函数,是因为这样可以保证在同一个tick 内多次执行nextTick,在一个tick里面完成渲染,不会开启多个异步任务。
// 举个栗子🌰 // 假如我们直接在nexttick里面直接执行回调 function nextTick (cb) { setTimeout(cb) } nextTick(cb1) nextTick(cb2) 这种情况下就会开启两个异步任务,也就是两次事件循环,造成了页面不必要的渲染
timerFunc是实现的核心,它会优先使用Promise等microtask,保证在同一个事件循环里面执行,这样页面只需要渲染一次。实在不行的话用setTimeout来兜底,虽然会造成二次渲染,但这也是最差的情况。vue在这里用了降级处理的策略。
$nextTick
最后再把nexttick函数挂到Vue原型上就OK了
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
}
小结
vue异步更新,本质上是js事件机制的一种运用,优先考虑了具有高优先级的microtask,为了兼容,又做了降级策略。
现在再回头看开头的那两个问题
-
for循环更新count数值,dom会被更新100次吗?
不会,因为queueWatcher函数做了过滤,相同的watcher对象不会被重复添加。
-
nextTick是如何做到监听dom更新完毕的?
vue用异步队列的方式来控制DOM更新和nextTick回调先后执行,保证了能在dom更新后在执行回调。