?Vue源码——订阅者的响应

时间:2021-1-8 作者:admin

前言

上篇专栏中介绍了发布者是如何收集订阅者(Watcher),本专栏来详细介绍发布者发生变化后,如何通知订阅者,而订阅者是如何响应。

一、如何通知订阅者

在 Vue 中发布者一般是数据,当数据发生变化了会触发数据 setter 函数,其定义在 defineReactive 函数中。

function defineReactive(obj, key, val, customSetter, shallow) {
    var dep = new Dep();
    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
        return
    }
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key];
    }
    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            //...
        },
        set: function reactiveSetter(newVal) {
            var value = getter ? getter.call(obj) : val;
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if (customSetter) {
                customSetter();
            }
            if (getter && !setter) {
                return
            }
            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            childOb = !shallow && observe(newVal);
            dep.notify();
        }
    });
}

可以看到在 setter 函数中,在执行完新值赋值逻辑后。执行 childOb = !shallow && observe(newVal),其中 shallow 是用来控制是否要对新值调用 observe 方法监听,若为 true 就不监听。

执行 dep.notify() 通知订阅者,来看一下 Dep 的实例方法 notify

Dep.prototype.notify = function notify() {
    var subs = this.subs.slice();
    if (!config.async) {
        subs.sort(function(a, b) {
            return a.id - b.id;
        });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
}

执行 var subs = this.subs.slice() 把订阅者克隆一份,避免后续操作影响原订阅者集合 this.subs

config.asyncfalse 时是同步更新,这里默认为 true 是异步更新。

最后遍历 subs 执行 subs[i].update(),其中 subs[i] 就是一个个订阅者,一个个 Watcher,那么 update 是 Watcher 的实例方法,而订阅者的响应就是从 update 这个实例方法开始。

二、订阅者的响应

先来看一下 Watcher 的实例方法 update

Watcher.prototype.update = function update() {
    if (this.lazy) {
        this.dirty = true;
    } else if (this.sync) {
        this.run();
    } else {
        queueWatcher(this);
    }
}

在其中对不同类型的订阅者进行各种的响应处理。

1、计算属性订阅者的响应

执行 if(this.lazy) ,实例对象 lazy 是计算属性订阅者的标志。若是计算属性订阅者,则 this.lazytrue,那么把实例对象 dirty 设置为 true,这有什么作用?要回到计算属性的 getter 函数中来看,其是在初始化计算属性过程中调用defineComputed 函数,又在其中调用 createComputedGetter 方法来定义的,来看一下 createComputedGetter 方法。

function createComputedGetter(key) {
    return function computedGetter() {
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }
            if (Dep.target) {
                watcher.depend();
            }
            return watcher.value
        }
    }
}

当第一次读取计算属性时,会触发其 getter 函数,也就是执行 computedGetter 函数,在其中执行 if (watcher.dirty),此时实例对象 dirty 是为 true,则会执行 watcher.evaluate()

Watcher.prototype.evaluate = function evaluate() {
    this.value = this.get();
    this.dirty = false;
}

在其中执行 this.value = this.get(),获取计算属性的值并赋值给实例对象 value,最后把实例对象 dirty 置为 false,并返回实例对象 value

假如计算属性订阅者订阅的发布者没有发生变化,再次去读取计算属性,此时实例对象 dirty 是为 false,不会去执行 watcher.evaluate(),直接返回实例对象 value。这也是计算属性缓存的原理。

假如计算属性订阅者订阅的发布者发生变化,就是会通知计算属性订阅者,其作出响应把实例对象 dirty 置为 true。当再次去读取计算属性时,就会执行 watcher.evaluate(),获取计算属性的新值赋值给实例对象 value,再把实例对象 dirty 置为 false,并返回实例对象 value。这样就完成了计算属性订阅者的响应。

2、渲染订阅者的响应

执行 else if (this.sync)this.sync 默认为 false,那么执行 queueWatcher(this),来看一下 queueWatcher 函数。

var queue = [];
var has = {};
var waiting = false;
var flushing = false;
var index = 0;
function queueWatcher(watcher) {
    var id = watcher.id;
    if (has[id] == null) {
        has[id] = true;
        if (!flushing) {
            queue.push(watcher);
        } else {
            var i = queue.length - 1;
            while (i > index && queue[i].id > watcher.id) {
                i--;
            }
            queue.splice(i + 1, 0, watcher);
        }
        if (!waiting) {
            waiting = true;

            if (!config.async) {
                flushSchedulerQueue();
                return
            }
            nextTick(flushSchedulerQueue);
        }
    }
}

这里引入一个队列的概念。当发布者通知订阅者,订阅者不是马上就响应的,而是先把订阅者添加到一个队列 queue 中。最后执行 nextTick(flushSchedulerQueue) ,在 nextTick 函数中调用 flushSchedulerQueue 函数,可以理解异步调用 flushSchedulerQueue 函数, 在flushSchedulerQueue 函数中订阅者进行响应,执行对应的更新,也就是异步执行更新。

那为什么要引入队列,为什么要异步执行更新。首先队列 queue 是个全局变量,当多个发布者发生变化时,都会通知其收集的订阅者。假设订阅者不添加到队列中,而直接响应。若是订阅者都不同那还好。要是订阅者们有相同的,是不是要重复响应。例如一个渲染订阅者重复响应好几次,那么 DOM 也要重复更新好几次,这就会影响性能了。

所以要引入队列,在添加到队列的过程中,执行通过 has 对象确保相同的订阅者只能添加一次到队列 queue 中。这样就避免了相同的订阅者重复响应好几次。再加上异步去遍历队列,让每个订阅者响应,进一步避免了相同的订阅者重复响应。

正如 Vue 官方文档所描述的

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

最后通过 wating确保在一个事件循环 tick (一个订阅者队列响应的过程)中对 nextTick(flushSchedulerQueue) 的执行只有一次,至于 nextTick 函数会专门开个专栏介绍。

来看一下 flushSchedulerQueue 函数,先去除 keep-alive 组件、组件钩子函数相关的逻辑。

var MAX_UPDATE_COUNT = 100;
var queue = [];
var has = {};
var circular = {};
var waiting = false;
var flushing = false;
var index = 0;
function flushSchedulerQueue() {
    flushing = true;
    var watcher, id;
    queue.sort(function(a, b) {
        return a.id - b.id;
    });
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        if (watcher.before) {
            watcher.before();
        }
        id = watcher.id;
        has[id] = null;
        watcher.run();
        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
            }
        }
    }
    resetSchedulerState();
}

先把 flushing 置为 true,为啥这么做后面介绍。

因为在创建订阅者 (Watcher)时,其实例对象 id 是自增加1,故通过执行 queue.sort(function(a, b) { return a.id - b.id;}) 给队列 queue 中的订阅者进行从小到大的排序。那为什么要进行排序,原因有以下两点。

  • 因为组件的更新由父到子,所以组件中的订阅者的响应顺序也应该先父后子。其次假设子组件在父组件中的订阅者响应期间被销毁,那么它对应的订阅者响应都可以跳过,所以父组件的订阅者应该先响应。又因为父组件是先于子组件创建的,所以父组件的订阅者也是先于子组件的订阅者创建的,那么可以通过排序来保证组件中的订阅者的响应顺序是先父后子。

  • 用户自定义订阅者的创建是在 initWatch 函数中调用 initWatch 创建的,渲染订阅者是在执行 vm.$mount 中创建的,因为 initWatch 函数先于vm.$mount 调用的,故用户自定义订阅者要先于渲染订阅者响应。

排序完后遍历队列 queue,在遍历中执行 watcher = queue[index] 获取一个订阅者(Watcher)赋值给常量 watcher

执行 if (watcher.before) ,其中 before 是 Watcher 的一个实例对象。其值是构造函数的参数 options 的属性 before 的值,this.before = options.before,在创建渲染 Watcher 时 参数options 的值是

{
    before: function before() {
        if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate');
        }
    }
}

那么 watcher.before 有值是个函数,则会执行 watcher.before()

执行 id = watcher.id,获取订阅者的标识 id,因为到这里这个订阅者已经进入响应流程了,为了下次还能把这个订阅者添加到队列 queue 中,所以要执行 has[id] = null

执行 watcher.run(),这里要注意,在其执行的过程中,可能会导致其他的发布者发生变化,这样就有新的订阅者要添加到队列中,则又去调用 queueWatcher 函数。因为在 flushSchedulerQueue 函数中,把 flushing 置为 true,所以在 queueWatcher 函数中执行 if (!flushing) 时会走 else 部分逻辑。

var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
    i--;
}
queue.splice(i + 1, 0, watcher);

在队列中从后往前找,找到新增订阅者的 id 比当前队列中订阅者的 id 大的第一个位置 i。把新增订阅者插入到队列中这个位置的后一个位置上,导致 queue.length 的发生了变化,故在遍历队列 queue 过程中,要不断地重新获取 queue.length

另外为了防止在一个事件循环过程中订阅者循环响应。用 circular 对象记录每个订阅者的响应次数,当次数超过常量 MAX_UPDATE_COUNT 时就会在控制台打印出错误信息,并 break 跳出循环。

下面来看一下 run 这个 Watcher 的实例方法

Watcher.prototype.run = function run() {
    if (this.active) {
        var value = this.get();
        if (value !== this.value || isObject(value) || this.deep) {
            var 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);
            }
        }
    }
};

执行 if (this.active)active 实例对象是用来判断该订阅者是否被销毁,默认为 true,销毁时把它置为 false

执行 var value = this.get(),就是渲染订阅者的响应。为什么呢?

渲染订阅者的响应是要去更新 DOM 。回想渲染订阅者的创建过程中,在 Watcher 构造函数中最后执行 this.value = this.lazy ? undefined : this.get(),在 this.get() 中会执行 value = this.getter.call(vm, vm),其中 this.getter 是构造函数的参数 expOrFn 的值,创建渲染 Watcher 时其值是 vm._update(vm._render(), hydrating),其中 vm._update 的作用是把 VNode 渲染成真实 DOM,具体介绍可以看这两篇专栏-(专栏一专栏二)。所以执行 var value = this.get(),就是渲染订阅者的响应。

执行 if (value !== this.value || isObject(value) || this.deep),因为在渲染订阅者中执行 this.get() 的返回值是 undefined 那么 value === this.value,故渲染订阅者的响应到此结束,至于怎么更新 DOM 后续将会专门开个专栏介绍。

3、用户自定义订阅者的响应

用户自定义订阅者的响应还是在 run 实例方法中进行的,接着上面继续往下看。

执行 if (value !== this.value || isObject(value) || this.deep),当用户自定义订阅者响应时,其新值 value 和旧值 this.value 肯定不相同,另外新值 value 可能是数组或对象,而 this.deep 也是用户自定义订阅者特有的实例对象,为 true 表示要深度监听发布者。

故满足判断条件,执行 var oldValue = this.value 把旧值赋值给常量 oldValue,再执行 this.value = value 把新值赋值给 this.value

执行 if (this.user) user 也是用户自定义订阅者特有的实例对象,其值为 true

在 try 代码块中执行 this.cb.call(this.vm, value, oldValue) ,执行用户自定义订阅者的回调函数,这就是用户自定义订阅者的响应。

在 catch 代码块中定义了,当执行用户自定义订阅者的回调函数失败了的处理。

若不是用户自定义订阅者,其他订阅者也执行 this.cb.call(this.vm, value, oldValue)

在遍历完订阅者队列后执行 resetSchedulerState() 把一些相关状态恢复。

function resetSchedulerState() {
    index = queue.length = activatedChildren.length = 0;
    has = {};
    circular = {};
    waiting = flushing = false;
}

三、总结

订阅者的响应实际上就是当数据发生变化的时候,触发其 setter 函数,使用 Dep 实例方法 notify 通知其收集的订阅者,也就是 Watcher,然后执行它们的 update 实例方法,在其过程中先把它们添加到一个队列中并做了去重的操作,在下一个事件循环后遍历这个队列,执行每个 Watcher 的 run 实例方法,在其中

  • 渲染订阅者是通过执行其实例方法 get 重新求值完成响应;
  • 计算属性订阅者是把其实例对象 dirty 置为 true,使再读取计算属性时会重新执行其实例方法 get 求值完成响应;
  • 用户自定义订阅者是执行其回调函数来完成响应。
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。