preface

As described in the previous column how publishers collect subscribers (Watcher), this column details how publishers notify subscribers of changes and how subscribers respond.

1. How to notify subscribers

In Vue, publishers are generally data, and when data changes, data setter functions are triggered, which are defined in the defineReactive function.

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];
    }
    varchildOb = ! 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();
        }
    });
}
Copy the code

You can see in the setter function, after the new value assignment logic is done. Execute childOb =! Shallow && observe(newVal), where shallow controls whether to call the observe method to listen on the new value or not if true.

Notify the subscriber by calling dep.notify() to take a look at the notify instance method of DEP.

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(); }}Copy the code

Execute var subs = this.subs.slice() to clone the subscriber to avoid subsequent operations affecting the original subscriber set this.subs.

Config. async is false for synchronous updates, which is true for asynchronous updates.

Subs [I].update() is executed through subs, where subs[I] is subscriber by subscriber, Watcher by Watcher, so update is Watcher’s instance method, and the subscriber’s response starts with update.

2. Response from subscribers

Take a look at the Watcher instance method update.

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

In it, various responses are processed for different types of subscribers.

1. Calculate the response of the attribute subscriber

Execute if(this.lazy), and the instance object lazy is the flag that calculates the attribute’s subscriber. If this. Lazy is true when counting attribute subscribers, what does setting instance objects dirty to true do? Going back to the getter function for calculating a property, which is defined by calling the defineComputed function during the initialization of the calculated property and calling the createComputedGetter method within it, take a look at the createComputedGetter method.

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
        }
    }
}
Copy the code

When a calculated property is read for the first time, its getter is triggered, that is, it executes the computedGetter function in which if (watcher.dirty) is executed, and the instance object dirty is true. Watcher.evaluate () is executed.

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

This.value = this.get() to get the value of the calculated property and assign the value to the instance object value. Finally, set dirty to false and return the instance object value.

If the evaluated property subscriber reads the evaluated property again without changing the publisher, the instance object dirty is false, and watcher.evaluate() is not executed, and value is returned. This is how property caches are computed.

If the publisher to which the calculated attribute subscriber subscribed changes, the calculated attribute subscriber is notified, and it responds by setting the instance object dirty to true. When the evaluated property is read again, watcher.evaluate() gets the new value of the evaluated property, assigns it to the instance object value, sets dirty to false, and returns the instance object value. This completes the calculation of the attribute subscriber’s response.

2. Render the subscriber’s response

Else if (this.sync), this.sync defaults to false, then queueWatcher(this). See 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); }}}Copy the code

Here’s the idea of a queue. When a publisher notifies a subscriber, the subscriber does not respond immediately, but adds the subscriber to a queue first. NextTick (flushSchedulerQueue); nextTick(flushSchedulerQueue); In the flushSchedulerQueue function, the subscriber responds and performs the corresponding update, that is, asynchronously.

So why queue, why do you do updates asynchronously. First, queue is a global variable that notifies its collected subscribers when multiple publishers change. Assume that the subscriber does not add to the queue and responds directly. It would be fine if all the subscribers were different. If the subscribers have the same, do not repeat the response. For example, if a render subscriber responds several times, the DOM will be updated several times, which can affect performance.

So to import the queue, the implementation ensures that the same subscriber can only be added to the queue once through the HAS object during addition to the queue. This prevents the same subscriber from responding multiple times. This, coupled with asynchronous traversal of the queue, allows each subscriber to respond, further avoiding repeated responses from the same subscriber.

As described in the official Vue documentation

Vue is executed asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation.

Finally, wating ensures that nextTick(flushSchedulerQueue) is executed only once in an event loop tick (the process by which a subscriber queue responds), and the nextTick function has its own column.

Take a look at the flushSchedulerQueue function and remove the logic associated with the keep-alive component and its hook function.

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();
}
Copy the code

So let’s set flushing to true, and we’ll talk about why we did that.

Queue. Sort (function(a, b) {return a.id -b.id; }) sort the subscribers in the queue from smallest to largest. So there are two reasons why we want to sort.

  • Because components are updated from parent to child, the response order of the subscribers in the component should also be parent to child. Second, if the child component is destroyed during the parent component’s subscriber response, then its corresponding subscriber response can be skipped, so the parent component’s subscriber should respond first. Since the parent component is created before the child component, the subscribers of the parent component are also created before the subscribers of the child component, so the order of response of the subscribers in the component can be sorted to ensure that the order of response of the subscribers in the component is the first parent and the second child.

  • The custom subscriber is created by calling initWatch in initWatch function, and the render subscriber is created by executing vm.$mount. Since initWatch function is called before vm.$mount, the custom subscriber responds before the render subscriber.

Watcher = queue[index]; watcher = queue[index]; watcher = queue[index]

Execute if (watcher. Before), where before is an instance object of watcher. The value is the value of the constructor’s options property before, this.before = options.before, which was created when the render Watcher was created

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

If the value of watcher.before is a function, then watcher.before() is executed.

Id = watcher. Id to get the subscriber id, has[id] = null to add the subscriber to the queue.

Execute watcher.run(), which may cause other publishers to change so that a new subscriber is added to the queue and the queueWatcher function is called. Because flushing is true in flushSchedulerQueue, we call queueWatcher if (! When flushing goes the else part of logic.

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

Look backwards in the queue to find the first position I where the id of the new subscriber is greater than the ID of the subscriber in the current queue. Inserting the new subscriber to the next position in the queue causes queue.length to change, so it is necessary to continually retrieve queue.length as it traverses the queue.

In addition, to prevent the subscriber from cyclic response during an event loop. Circular objects are used to record the number of times each subscriber has responded, and when the number of times exceeds the constant MAX_UPDATE_COUNT an error message is printed on the console and break out of the loop.

Let’s take a look at the run Watcher instance method

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); }}}};Copy the code

Execute if (this.active). The active instance object is used to determine whether the subscriber has been destroyed. The default value is true, and the value is set to false when the subscriber is destroyed.

Var value = this.get() renders the subscriber’s response. Why is that?

The response to render the subscriber is to update the DOM. Recall that during the creation of the render subscriber, this.value = this.lazy was last called in the Watcher constructor. undefined : This.get (), value = this.getter. Call (vm, vm), where this.getter is the expOrFn parameter value of the constructor, Vm._update (vm._render(), hydrating). Vm._update (vm._render(), hydrating) is used to render vnodes into real DOM. So var value = this.get() renders the subscriber’s response.

Execute if (value! = = this. Value | | isObject (value) | | this. Deep), because in rendering the subscriber to perform this. The get (), the return value is undefined value = = = this. Then the value, Rendering the subscriber’s response ends here, and there will be a column on how to update the DOM.

3. User customization of subscribers’ responses

User-defined subscriber responses are also done in the Run instance method, which continues above.

Execute if (value! = = this. Value | | isObject (value) | | this. Deep), when a user custom subscriber response, the new value, the value and the old value. This value must be different, and the new value, the value may be an array or object, This.deep is also a user-defined subscriber specific instance object, and true means deep listening for publishers.

Var oldValue = this.value assigns the oldValue to the constant oldValue, and this.value = value assigns the new value to this.value.

Execute if (this.user), which is also a user-definable subscriber specific instance object with a value of true.

Call (this.cb.call(this.vm, value, oldValue)) in the try block executes the user defined subscriber callback function, which is the user defined subscriber response.

In the catch block, we define what to do when a user – defined subscriber callback fails.

Call (this.cb.call(this.vm, value, oldValue) is also executed by other subscribers who are not user-defined subscribers.

After traversing the subscriber queue, resetSchedulerState() is executed to restore some of the associated states.

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

Third, summary

The subscriber’s response is essentially to fire its setter function when the data changes, notify its collected subscribers using the Dep instance method notify, or Watcher, and then execute their Update instance method, After the next event loop, the queue is iterated through, executing each Watcher’s run instance method in the

  • The subscriber is rendered by executing the example methodgetRe-evaluate to complete the response;
  • Calculating attribute subscribers is taking the instance objectdirtySet totrueSo that the example method is re-executed when the calculated property is read againgetEvaluate to complete the response;
  • The user – defined subscriber executes its callback function to complete the response.