Vue源码学习--异步更新dom

643 阅读4分钟

偶然间看到一位大佬关于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);
    }

排除Watcherlazy以及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')
})

结果:

image.png

扩展:关于宏任务和微任务

宏任务:

可以理解是每次执行栈执行的代码就是一个宏任务。

  • 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' //就只修改一个参数

结果

image.png

2、上述nextTick的案例,证明nextTick函数内的pending的作用。

结果

image.png