偶然间看到一位大佬关于nextTick方法的文章,再加上之前自己虽然经常用,但从来没有去尝试理解一下原理。然后说干就干,不看不知道,一看竟然发现这中间会牵扯到Vue的异步更新DOM机制,简直太厉害了😻,赶快记录一下!
我们都知道Vue最基础的就时双向绑定原理,具体的内容这里不详细去研究了。但其中扩散一些功能和知识点,也更是精髓所在。
此处送触发更新机制的一个很小的流程开始记录。(来自菜鸟前端的自学记录,望大佬指出错误!😆)
1、mountComponent
根据更新机制,我们先简单看一下定义Watcher的代码。因为最终去触发更新DOM这个操作的重要角色就是它了。遍历虚拟节点转为浏览器可识别DOM元素时,为对应的标签元素挂载watch对象,同时当data改变出发Deps(通知watcher的一个集合)通知watcher去执行update方法。
function mountComponent (
//。。。
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
//。。。
}
2、Watcher.prototype.update
此处重点讨论数据改变时,异步渲染到DOM的流程,其他就不过多概述。
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else { //正常流程会运行到这边
console.log('触发watcher的update')
queueWatcher(this);
}
排除Watcher的lazy以及sync属性的情况,会去运行queueWatcher方法,并把当前的watcher传入到函数里。那么我们继续往下看,感觉一步一步往下走,就越来越接近核心内容了。
3、queueWatcher
function queueWatcher (watcher) {
console.log('当前watcher',watcher)
var id = watcher.id; //赋值当前watcher的id
if (has[id] == null) { //此处用个has对象记录当前loop下的watcher,无值的情况才会继续往下走,避免重复
has[id] = true;// 标记当条含有此id的watcher已经存入了
console.log('flushing状态',flushing)
if (!flushing) { // 此处的flushing用来记录当前的queues是否在运行,后面会讲到它的具体用处
queue.push(watcher); //往一个队列里塞入watcher,注意,此处已经开始异步更新的操作了,因为这边没有之前去运行
console.log('存入watcher之后,queue的值',queue)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
console.log('waiting状态',waiting)
if (!waiting) {
console.log('为false的情况,置为true')
waiting = true; // 每次loop循环开始时,waiting置为true,之后的update方法就不会调用下面的方法,既然不会调用了,那么其实就可以想到这边的nextTick就是我们在项目用到的方法。简单点说,就是把执行循环queue的方法,以微任务或其他方式统一推到正常代码(宏任务之后),以此解决频繁刷新的问题。
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue();
return
}
console.log('waiting=true,调用nextTick,让flushSchedulerQueue方法稍后执行')
nextTick(flushSchedulerQueue); // 调用nextTick,把flushSchedulerQueue放入其中
}
}
字段解释:
✔️has:
此处用个has对象记录当前loop下的watcher,确保此条没有被记录的情况才会继续往下走,避免同一个属性多次修改多次触发,避免重复。
✔️while (i > index && queue[i].id > watcher.id):
这边看了挺久,直到往后看才恍然大悟。
后面会讲到flushSchedulerQueue这个方法,在它运行时,会将flushing置为true的同时,把queue升序排列。所以在执行flushSchedulerQueue的同时,如果有新的watcher进来,这个方法的作用就来了,i开始为数组最大值,i以此循环减一,知道i等于index(排序的作用,当前queue里面执行到第几个了)的时候,把当前watcher插入到这之后,立马去当成下一个运行。
4、flushSchedulerQueue
首先来看flushSchedulerQueue方法,之后再看nextick方法。
function flushSchedulerQueue () {
console.log('flushSchedulerQueue内部')
console.log('flushing置为true,表示当前队列运行中')
currentFlushTimestamp = getNow();
flushing = true; //此处对应到的就是之前提到的,在运行此方法时,flushing 状态为true,在此方法运行中,
var watcher, id;
console.log('本次循环queue的最终内容',queue)
// Sort queue before flush.
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) { //遍历运行queue数组
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
//。。。省略一些代码
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState(); //重置属性
// call component updated and activated hooks
callActivatedHooks(activatedQueue); // actived周期,与keep-alive配合
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
所以,flushSchedulerQueue方法其实就是最终的遍历运行queues里存储的watcher的目的,只不过它是等待当前一个loop里的宏代码运行完(简单说就是往queues里塞值的过程),最后再将一些属性重置。
5、nextTick
一样直接上源码
function nextTick (cb, ctx) {
console.log('nextTick内部,将方法存入callbacks')
var _resolve;
callbacks.push(function () { //此处将cb回调方法传入callbacks数组
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
console.log('callbacks:',callbacks)
console.log('pedding状态',pending)
if (!pending) { //此处pending用来控制状态,下面会详细讲到
console.log('pedding置为',true)
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
字段解释:
✔️pedding
pending:此处的pending应该是针对多个nextTick方法的,当前的update里第一个nextTick运行时,对应的回调放入callbacks数组里,等待promise.then(微任务去调用);
如果此时还有接下来的的nextTick方法,为了使其不再去对同一个callbacks数组执行promise.then,通过pending来判断;当pending为true时,后面的nextTick只会将回调放入callbacks数组,不会再去使其等待运行,而是等第一次的promise一起调用。(下面会有个例子的运行结果)
6、只剩下最后一个timerFunc方法啦,加油!😄
var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
console.log('将callbacks放入promise.then里面去运行')
p.then(flushCallbacks); //这边就用到了Promise.then()实现去调整任务调用顺序
//同时,flushCallbacks数组就是对callbacks(回调缓存数组的复制值)
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//。。。。。。
//此处省略一些不相关代码,是用来判断当前浏览器不支持promise的情况
}
此处的方法简单来看就是实际去运行callbacks里方法的地方,只不过控制了运行时机。到这边,其实整个流程就简单走完了,其实主要是其中一些变量的作用要好好想一想,把这些理顺了后,其实原理挺简单的。(看懂只是第一步,后面要走的路还远着呢。。)
最后再总结一下:
触发Watcher.prototype.update方法 ⏩ 运行queueWatcher方法(存入队列和控制状态的过程) ⏩ 定义最终去执行队列内方法的方法flushSchedulerQueue ⏩ 上述方法利用nextTick控制何时去执行,达到异步更新的效果。
需要学习的东西还有很多,接下来继续努力吧。🌼🌼🌼
然后
由此可以理解到有时候在改变data的值后,又要获取对应DOM的值时,需要用到Vue.$nextTick方法,保证watcher的更新DOM方法完成后再去取值。
例如:
<div>{{msg}}</div>
//msg: 'a'
this.msg = 'b'
console.log(this.$refs.msgRef.innerHTML,'after change')
this.$nextTick(()=>{
console.log(this.$refs.msgRef.innerHTML,'after $nextTick')
})
结果:
扩展:关于宏任务和微任务
宏任务:
可以理解是每次执行栈执行的代码就是一个宏任务。
- script(整体代码)
- setTimeout
- setInterval
- I/O UI交互事件
- postMessage
- MessageChannel
- setImmediate(Node.js 环境)
微任务:
microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
- Promise.then
- Object.observe
- MutationObserver
- process.nextTick(Node.js 环境)
这边不过多解释,只要知道这边的运行顺序是script ⏩ Promise.then ⏩ setTimeout,估计只有我这种菜鸟才是刚理解到这些东西吧😭。
补充一下两种情况的运行打印截图(打印位置上述源码可看到)
1、当修改一个值时
this.msg = 'a' //就只修改一个参数