• 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述
     由此可以看出,当我们对两种数据进行修改时,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);
            }
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

     从这里我们可以发现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);
                }
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

     通过这段代码我们就可以发现,当调用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');
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

     首先先将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源码系列文章:

  • 相关阅读:
    享元模式【Java设计模式】
    编译相关内容(自用)
    父子进程、僵尸进程和孤儿进程
    leetcode做题笔记126. 单词接龙 II
    模板进阶:非类型模板参数,特化
    微服务架构演进
    【Python百日进阶-数据分析】Day122 - Plotly Figure参数: 散点图(四)
    软件测试面试复习题(一)
    2023考研常识分享之英语一与英语二有哪些区别?
    static_cast与dynamic_cast到底是什么?
  • 原文地址:https://blog.csdn.net/liu19721018/article/details/125480641