vue 原型上面有一个$nextTick方法,它的主要用途是用来获取组件更新后的DOM节点。
它的实现原理是在它的内部实现了一个名为nextTick的方法。用来处理组件Dom的异步更新。
vue2.0
- 首先它内部创建了一个
callbacks数组,可以称它为回调队列(类似于js的异步更新队列callback Queue); - 每次调用
nextTick方法时,会在callbacks里面push一个方法,这个方法会执行传入的回调函数。 - 接下来会判断一个初始值为
false的pending变量,它标志将来是否需要遍历callbacks数组。
如果
pending为true,就会跳过。如果pending的值为false, 则将pending设置为true,然后执行timerFunc函数,它的内部是实现是以异步的方式(微任务或者宏任务)调用flushCallbacks方法。它主要作用是遍历执行callbacks数组的每一个方法,也就是我们调用nextTick时传入的回调方法,同时也会将callbacks清空,pending设置为false。
timeFunc函数的默认方式是用Promise实现异步,如果当前环境不支持,就会做降级处理,使用MutationObserver。这两个都是微任务,如果还是不支持,就使用setImmediate,最后使用 setTimeout,这两个是宏任务。
- 当组件内的一个响应式数据发生变化时,会触发它收集到的依赖,也就是
Wather实例(watch选项,computed选项,$watch方法,组件更新函数),但是并不会立即去执行这些依赖的run方法执行回调函数,而是调用queueWatcher方法把这些Watch实例放在queue更新队列,同时会做去重处理(watcher.id),(这个时候也有可能队列正在刷新,flushing == true),如果正在刷新队列,根据id从后往前查找第一个小于正在执行的Wather.id,并放到它的后面这个方法内部会执行nextTick方法,它传入的回调函数(flushSchedulerQueue)就是去刷新这个队列,执行run方法,执行回调函数。 - 但是并不是每个响应式数据发生变化时都会调用
nextTick。它会判断一个初始值为false的waiting变量(它表示是否正在等待刷新这个队列); 如果waiting为true,表示正在等在刷新着异步更新队列,不需要重新去刷新,就会跳过。如果waiting为false,则将waiting = true,则执行nextTick,等待将来某个时候时候刷新队列。 flushSchedulerQueue函数执行时(刷新队列),会将初始值为false的waiting变量设置为true,表示是否正在刷新队列,同时也会更新id进行排序操作。- 如果在响应式数据发生变化之前,手动调用了
nextTick方法,它的回调函数会放在回调队列callbacks的首位 ,里面是不能获取组件更新后的DOM节点,因为响应式数据发生变化之后内部执行nextTick时,它的回调函数flushSchedulerQueue会依次放到到回调队列callbacks里,在我们手动调用的后面,所以没办法获取组件更新后的 DOM 节点。 - 为什么队列需要从小到大(
Wather.id)排序?
- 父组件先于子组件更新,因为父组件肯定先于子组件创建。
- 组件自定义的
watcher将先于渲染watcher执行,因为自定义watcher先于渲染watcher创建。- 如果组件在父组件执行
wtcher期间destroyed了,它的watcher集合可以直接被跳过。
class Watch {
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
},
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
Vue.prototype.$destroy = function () {
var vm = this;
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy');
vm._isBeingDestroyed = true;
// remove self from parent
var parent = vm.$parent;
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm);
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown();
}
var i = vm._watchers.length;
while (i--) {
vm._watchers[i].teardown();
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--;
}
// call the last hook...
vm._isDestroyed = true;
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null);
// fire destroyed hook
callHook(vm, 'destroyed');
// turn off all instance listeners.
vm.$off();
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null;
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null;
}
};
}
vue3.0
3.0版本对这个方法做了不一样的处理。它没有做任何的降级处理,默认支持Promise,通过Promise的链式操作(.then),确保在组件更新以后执行$nextTick传入的回调方法,看上去更好理解一点。
如果先执行
queueFlush,是链式操作,如果先执行$nextTick,则是微任务队列。
代码如下:
const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise;
return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
...
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy)),
...
// 更新队列将isFlushing, 未来发生。
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}