Vue源码学习之异步更新原理

前言

 首先我们了解到Vue的Dom更新是异步的,当我们更新数据后,立即获取Dom的内容,此时Dom的内容还是旧的内容,那么我们可以通过$nextTick在回调函数中去获取最新的Dom内容,那么这个时候就有所考虑了,为什么就有所考虑了,为什么我们不在Vue中所操作的Dom是同步的,在Vue中确是异步的呢?实际跟Vue的渲染机制有关,在Vue中当修改数据后,此时渲染是异步的,所以Dom的更新也是为异步的。

Dom的更新是批量的?

 Vue中Dom的更新不仅仅是异步的而且还是批量的,这样做的好处是能够最大程度的优化性能,对于相同Dom的多次修改,我们只需要赋值最后一次修改的结果即可,演示如下:

<template>
  <div class="hello">
    <span>名字:{
    
    {
    
     name }}</span>
    <span> | </span>
    <span>年龄:{
    
    {
    
     age }}</span>
    <span> | </span>
    <span>渲染次数:{
    
    {
    
     updateCount }}</span>
  </div>
</template>

<script>
export default {
    
    
  name: "HelloWorld",
  data() {
    
    
    return {
    
    
      name: "小飞",
      age: 18,
      updateCount: 0,
    };
  },
  mounted() {
    
    
    this.name = "小哲";
    this.age = 30;
  },
  updated() {
    
    
    this.updateCount++;
  },
};
</script>

在这里插入图片描述
 由此可以看出,当我们对两种数据进行修改时,updated钩子函数只会执行一次,也就我们同时更新了多个数据,Dom只会更新一次。

源码分析

异步更新队列

 在Vue中DOM更新一定是由于数据变化引起的,当数据变化时,会触发数据劫持中的setter,使其调用dep实例的notify方法,通知所有订阅者watcher进行更新,源码如下:

    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    Watcher.prototype.update = function update() {
    
    
        // 懒执行会走这里, 比如computed
        if (this.lazy) {
    
    
            this.dirty = true;
        // 同步执行会走这里,比如this.$watch() 或watch选项,传递一个sync配置{sync: true}
        } else if (this.sync) {
    
    
            this.run();
        } else {
    
    
            // 将当前watcher添加到watcher队列中,默认走此else
            queueWatcher(this);
        }
    };

 从这里我们可以发现vue默认就是走的是异步更新机制,默认调用queueWatcher方法将当前这个watcher加入到异步队列中,下面我们看一下queueWatcher方法:

    /**
     * Push a watcher into the watcher queue.
     * Jobs with duplicate IDs will be skipped unless it's
     * pushed when the queue is being flushed.
     */
    function queueWatcher(watcher) {
    
    
        var id = watcher.id;
        // 判断watcher是否被标记过,如果标记过则不执行,避免重复推入watcher
        if (has[id] == null) {
    
    
            // 没被标记过的watcher则进行标记
            has[id] = true;
            // 如果flushing为false, 表示当前watcher队列没有在被刷新,则watcher直接进入队列
            if (!flushing) {
    
    
                queue.push(watcher);
            } else {
    
    
                // if already flushing, splice the watcher based on its id
                // if already past its id, it will be run next immediately.
                
                // 如果watcher队列已经在刷新了,这个时候我们插入对应的watcher时,就需要对即将插入的watcher进行排序插入
                var i = queue.length - 1;
                while (i > index && queue[i].id > watcher.id) {
    
    
                    i--;
                }
                queue.splice(i + 1, 0, watcher);
            }
            // queue the flush
            // 当waiting为true时,代表flushSchedulerQueue正在执行,当执行完毕后waiting会更新
            
            if (!waiting) {
    
    
                waiting = true;

                if (!config.async) {
    
    
                    flushSchedulerQueue();
                    return
                }
                // 将flushSchedulerQueue放在下一个事件循环中进行执行
                nextTick(flushSchedulerQueue);
            }
        }
    }

 通过这段代码我们就可以发现,当调用queueWatcher时会判断当前watcher是否已经添加在队列中,没有添加到队列中才会继续执行,然后判断当flushing标志锁为false,说明flushSchedulerQueue(刷新watcher队列)还没有执行,所以仍然可以继续将watcher添加到队列中,当已经执行刷新队列时,那么就将watcher通过id进行排序插入到合适的位置。当waiting这个标志锁为true时,说明正在执行刷新队列,则跳过执行,当为false的时候,说明没有执行刷新队列则调用nextTick去执行此方法,对应nextTick原理,可以看这篇文章Vue源码学习之nextTick,下面我们分析下flushSchedulerQueue的源码:

/**
     * Flush both queues and run the watchers.
     */
    function flushSchedulerQueue() {
    
    
        currentFlushTimestamp = getNow();
        // 将flushing设置为true(标志锁)
        flushing = true;
        var watcher, id;

        // flush队列前先排序
        // 目的是
        // 1. Vue中的组件的更新是从父组件到子组件(因为父组件总是比子组件先创建)
        // 2. user watcher比render watcher执行要早(因为user watcher比render watcher创建要早一些)
        // 3. 如果父组件的watcher调用run时将父组件干掉了,那其子组件的watcher也就没必要调用了


        // 通过watcher id进行排序
        queue.sort(function (a, b) {
    
    
            return a.id - b.id;
        });

        // 依次执行watcher的run方法,进行更新dom
        for (index = 0; index < queue.length; index++) {
    
    
            watcher = queue[index];
            if (watcher.before) {
    
    
                watcher.before();
            }
            id = watcher.id;
            has[id] = null;
            // 更新dom
            watcher.run();
            // dev环境下,检测是否为死循环
            if (has[id] != null) {
    
    
                circular[id] = (circular[id] || 0) + 1;
                if (circular[id] > MAX_UPDATE_COUNT) {
    
    
                    warn(
                        'You may have an infinite update loop ' + (
                            watcher.user ?
                            ("in watcher with expression \"" + (watcher.expression) + "\"") :
                            "in a component render function."
                        ),
                        watcher.vm
                    );
                    break
                }
            }
        }

        // keep copies of post queues before resetting state
        var activatedQueue = activatedChildren.slice();
        var updatedQueue = queue.slice();

        // 刷新flushing为false,waiting也为false,便于下次开启队列
        resetSchedulerState();

        // call component updated and activated hooks
        callActivatedHooks(activatedQueue);
        callUpdatedHooks(updatedQueue);

        // devtool hook
        /* istanbul ignore if */
        if (devtools && config.devtools) {
    
    
            devtools.emit('flush');
        }
    }

 首先先将flushing变为true,说明已经开始执行刷新队列了,那么后续如果在继续添加watcher的话,则使用watcher id对其排序添加到队列中,然后依次执行队列中watcher的run方法进行更新dom(diff更新),当刷新完毕后执行resetSchedulerState方法,将waiting和flushing标志锁变为false,便于下次开启队列,那么由此我们可以得出,当数据更新时,会将对应的watcher添加到异步队列中,在下一个事件循环中去执行刷新队列的方法,进行Dom更新。

总结

 当数据被赋值变化时,会触发dep实例的notify方法,使其依次调用watcher的update方法,在update方法中会将其watcher加入到异步队列中,并且无法重复加入相同的watcher,将刷新队列的方法通过nextTick使其在下一个事件循环中进行执行,刷新队列时,依次调用watcher的run方法进行更新dom。

Vue源码系列文章:

猜你喜欢

转载自blog.csdn.net/liu19721018/article/details/125480641