I have been to a lot of interviews recently, and I was asked about Vue source code in almost every interview. Open a series of columns here, to summarize this experience, if you feel helpful, might as well point to support the bai.

preface

The data monitoring process was explained in detail in the last column. In Vue, the design pattern of publisher and subscriber is adopted to realize this function. The publisher is the data, the subscriber is the Watcher, and the Dep is used to collect and manage the subscribers. There are three types of subscribers: Render Watcher, Computed Watcher, and User Watcher. This article focuses on how these three types of subscribers are collected.

A collection of render Watcher subscribers

As described in this column, subscribers are collected in the getter function that fires when data is read.

So when do you read the data? Remember that in this column, there is an important piece of logic for executing the mountComponent function during the Vue mount process:

var updateComponent; updateComponent = function() { vm._update(vm._render(), hydrating); }; new Watcher(vm, updateComponent, noop, { before: function before() { if (vm._isMounted && ! vm._isDestroyed) { callHook(vm, 'beforeUpdate'); } } }, true /* isRenderWatcher */ );Copy the code

Executing new Watcher instantiates the Watcher class, resulting in an instantiated object that is the subscriber, called Watcher. Take a look at the Watcher constructor.

Because there are three types of Watcher in Vue: Render Watcher, computed Watcher, and User Watcher, many instance objects must be defined in the constructor to implement these types of Watcher.

Let’s simplify the Watcher constructor by keeping only instance objects related to Render Watcher and Dep.

var uid = 0; var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) { this.vm = vm; if (isRenderWatcher) { vm._watcher = this; } vm._watchers.push(this); if (options) { this.deep = !! options.deep; this.lazy = !! options.lazy; this.before = options.before; } else { this.deep = this.lazy = false; } this.cb = cb; this.id = ++uid; this.deps = []; this.newDeps = []; this.depIds = new Set(); this.newDepIds = new Set(); if (typeof expOrFn === 'function') { this.getter = expOrFn; } this.value = this.lazy ? undefined : this.get(); };Copy the code
  • parametervm: Vue instantiates an object.
  • parameterexpOrFn: The data to be monitored, either as a string representing the data path to be observed or as a result of a function that returns the data to be observed.
  • parametercb: callback function, called when the monitored data changes.
  • parameteroptions: Some configuration options.
  • parameterisRenderWatcher: in order totrueIndicates that the Watcher created is a render Watcher.

This. deps and this.newDeps represent an array of dePs that Watcher holds for collecting and managing subscribers. So why do we need to have a collection of arrays with two dePs? I’ll talk about that later.

NewDepIds and this.newDepIds represent sets of identifiers for Dep in this.deps and this.newDeps, respectively. A Set is an ES6 data structure. There’s no repetition.

Finally, execute this.value = this.lazy? undefined : This.get (), because there is no lazy property in the options parameter of the Watcher constructor in this scenario, this.lazy is false and this.get() is executed. Take a look at the get instance method.

Watcher.prototype.get = function get() {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
        value = this.getter.call(vm, vm);
    } catch (e) {
        if (this.user) {
            handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
        } else {
            throw e
        }
    } finally {
        if (this.deep) {
            traverse(value);
        }
        popTarget();
        this.cleanupDeps();
    }
    return value
};
Copy the code

Let’s do pushTarget(this), let’s look at the pushTarget function,

Dep.target = null;
var targetStack = [];
function pushTarget(target) {
    targetStack.push(target);
    Dep.target = target;
}
function popTarget() {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
}
Copy the code

In the pushTarget function, dep. target is a static property of Dep. Executing dep. target = target assigns the current Watcher to dep. target, ensuring that only one Watcher can be collected at a time. Execute targetStack.push(target) to add the current Watcher to the targetStack array, which is used to restore the Watcher.

Within the popTarget function, execute targetStack.pop() to remove the current Watcher from the targetStack array, indicating that the current Watcher has been collected. Run dep. target = targetStack[targetStack.leng-1] to assign the last uncollected Watcher to dep. target.

The targetStack array acts like a stack, ensuring Watcher’s collection order. Why this is done will be discussed in a future scenario.

Value = this.getter.call(vm, vm) = this.getter.call(vm, vm)

if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
} 
Copy the code

You can see this. The value of the getter is a Watcher expOrFn constructor parameters, then this. Getter is the value of the function () {vm. _update (vm) and _render (), hydrating); }; So this.getter.call(vm, vm), equivalent to vm._update(vm._render(), hydrating), will execute vm._render() first.

Recall from this column that the vNode generation performed by vm._render() reads data from data, triggering a getter function for the data, where subscribers are collected. The getter function is defined in the defineReactive function.

function defineReactive(obj, key, val, customSetter, shallow) { var dep = new Dep(); 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() { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter(newVal) { } }); }Copy the code

Var dep = new dep () instantiates a dep class and assigns a value to the constant dep.

In the getter function, execute if (dep.target), where dep.target has a value, which is the current Wacther. Dep. depend() is the instance method of dep. Take a look at the Dep constructor first

var uid = 0;
var Dep = function Dep() {
    this.id = uid++;
    this.subs = [];
};
Copy the code

The Dep constructor is very simple. The ID instance object is the identifier of the Dep and increments by 1 every time a Dep is created. The Subs instance object is a container for collecting subscribers.

Take a look at the dep.depend instance method again.

Dep.prototype.depend = function depend() { if (Dep.target) { Dep.target.addDep(this); }}Copy the code

AddDep (this) is an instance method of Wachter, and Dep is actually a management of Watcher. It doesn’t make sense for THE Dep to exist alone from Watcher, so this article will interweave Dep with Wachter.

Take a look at the addDep instance method.

Watcher.prototype.addDep = function addDep(dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id);
        this.newDeps.push(dep);
        if (!this.depIds.has(id)) {
            dep.addSub(this);
        }
    }
}
Copy the code

Var id = dep.id; assign dep’s instance object ID to constant ID. Instance object ID is an identifier of DEp. Each DEP has a different ID.

Perform the if (! This.newdepids.has (id)), where this.newdepids is a Set of ID identifiers for Dep, so we use has to determine whether id already exists in this.newdepids.

If no, run this.newdepids.add (id) to add the ID to this.newdepids. Execute this.newdeps.push (dep), where this.newdeps is a collection of dePs, adding the created dePs from the currently subscribed publisher to this.newdeps.

Perform the if (! This.depids.has (id)) determines whether an ID already exists in this.depids, where this.depids is also a set of ID identifiers for Dep, and what is the difference between this.newdepids and this. If not, execute dep.addsub (this), and return to dep’s instance method addSub, where this is the current Watcher.

Take a look at Dep’s instance method addSub.

Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
}
Copy the code

Execute this.subs.push(sub) to collect subscribers, where sub is Watcher, which is the subscriber, and this.subs is the container to collect subscribers.

This completes the process of a subscriber collection. Does that end there? Well, it doesn’t, because the logic is executed in the try statement, and the logic is executed in the finally statement whether or not the execution succeeds.

finally {
    if (this.deep) {
        traverse(value);
    }
    popTarget();
    this.cleanupDeps();
}
Copy the code

In this scenario, the Watcher constructor does not have the “deep” property in the options argument, so this. Deep is false and the “if” code is not executed. Execute popTarget() directly. The popTarget function assigns the last uncollected Watcher to dep.target, as described earlier.

So why did Vue design this way? Let me give you an example. Suppose that the collection of subscriber A is triggered during rendering, but that the collection of Subscriber B is triggered during collection. In Vue, only one subscriber can be collected at the same time and place in order to ensure the order of notifying subscribers to execute the update. Therefore, the working principle of stack is cleverly utilized to realize the collection of subscriber B first, and then collect subscriber A after the collection of subscriber B is completed.

In Vue, create an array, targetStack, to store all subscribers that are triggered to collect but are not collected. Use pushTarget to simulate the push function and popTarget to simulate the stack. Guarantee the order of subscribers collected by the publisher.

Execute this.cleanupdeps () to look at the cleanupDeps instance method

Watcher.prototype.cleanupDeps = function cleanupDeps() { var i = this.deps.length; while (i--) { var dep = this.deps[i]; if (! this.newDepIds.has(dep.id)) { dep.removeSub(this); } } var tmp = this.depIds; this.depIds = this.newDepIds; this.newDepIds = tmp; this.newDepIds.clear(); tmp = this.deps; this.deps = this.newDeps; this.newDeps = tmp; this.newDeps.length = 0; }Copy the code

The cleanupDeps instance method is used to remove and purge unwanted publishers, that is, to notify the publisher to remove subscribers. So let’s look at the implementation logic,

Since Vue is data-driven, it will render again every time the data changes, and the vm._render() method will execute again, triggering the getter of the data again to collect the subscriber, so the Wathcer constructor will initialize an array of 2 dePs. NewDeps is the set of publishers that subscribed to, this.newdeps is the set of publishers that subscribed to the new, and this.deps is the set of publishers that subscribed to the last time.

If the instance object ID of Dep does not exist in this.newdepids, then dep.removesub (this) is executed to remove the subscriber’s subscription to this publisher. Then swap this.newdepids with this.depids, swap this.newdeps with this.deps, and empty this.newdepids and this.newdeps.

To explain why you need to clear the subscribers, consider A scenario where v-if is used to control module A and module B on A page. When v-if is true, rendering module A will read the data in module A, triggering the data in module A to collect the render Watcher (subscriber) for the page. If v-if is false, render module B will read the data in module B, triggering the data in module B to collect the render Watcher (subscriber) for the page. If the data in module A is changed at this point, the subscriber is notified to perform the update as well. Although the data of module A has changed, module A is no longer displayed on the page, and such an update is obviously wasteful.

If each time a subscriber is collected, the publisher set recorded in the subscriber for a new subscription is compared to the publisher set recorded in the last subscription, and the publishers who are not re-subscribed are notified to call the dep.removeSub instance method to clear the subscriber. There is no waste in updating.

Finally, take a look at the dep.removesub instance method

Dep.prototype.removeSub = function removeSub(sub) {
    remove(this.subs, sub);
};
function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}
Copy the code

The dep.removeSub instance method is simple. It calls remove to remove sub from the this.subs subscriber container. Sub is a subscriber to be removed.

In summary, during the page rendering process, a render Watcher (subscriber) is created during the mount phase. In the Watcher constructor, the get instance method is finally executed, where the vNode is generated by calling the vm._render() method. Dep’s instance method depend is called in the getter function that triggers the data to be read. The publisher’s collector Dep is stored in Watcher’s newDepIds, and then Dep’s instance method addSub is called to collect the subscriber. Finally, Watcher’s instance method, cleanupDeps, is called to cleanup unwanted publishers by traversing the deps collection of last subscribed publishers, To determine whether the last subscribed publisher is in the newDeps collection, use the Dep instance method removeSub to remove the subscriber from the publisher. After the cleanup, newDeps and deps are swapped, newDepIds and depIds are swapped, and newDeps and newDepIds are emptied, which completes the subscriber collection process.

A collection of user – defined subscribers

The collection process for user-defined subscribers is similar to that for rendered subscribers, but there are some differences in the collection process because user-defined subscribers can implement some additional features, such as deep listening, immediate callback, and unlistening. Here’s a look at the process of collecting user-defined subscribers.

1. Create internal logic for user – defined subscribers

User defined subscribers are Watcher defined by the option Watch or VM.$watch. Let’s first look at the internal logic for creating user-defined subscribers with the watch option.

The watch option is initialized with the following code in the initState function.

function initState(vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
    }
}
Copy the code

Which opts. Watch! Var nativeWatch = ({}).watch; var nativeWatch = ({}).watch; So we have to rule it out. Look again at the initWatch function.

function initWatch(vm, watch) { for (var key in watch) { var handler = watch[key]; if (Array.isArray(handler)) { for (var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); } } else { createWatcher(vm, key, handler); }}}Copy the code

Recall the use of the watch option in the official documentation

Watch: {// a: function(val, oldVal) {} b: 'someMethod', c: {handler: function(val, oldVal) {/*... */}, 'someMethod', immediate: true}, // [ 'handle1', function handle2(val, oldVal) { /* ... */ }, { handler: function handle3(val, oldVal) { /* ... */ }, /* ... */ } ], }Copy the code

Go back to the initWatch function and iterate over the watch so that each handler can be a function, an object, or an array. If the handler is an array, this will be done again, and createWatcher will be called for each item. If not, createWatcher will be called for each item. Look at the createWatcher function.

function createWatcher(vm, expOrFn, handler, options) {
    if (isPlainObject(handler)) {
        options = handler;
        handler = handler.handler;
    }
    if (typeof handler === 'string') {
        handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
}
Copy the code

$watch(expOrFn, handler, options);}} User Watcher is created using vm.$watch instance method

$watch(expOrFn, callback, [options]);}} $watch(expOrFn, callback, [options]);}}

  • expOrFnThe data to listen on, either as a string representing the data path to be observed, or as a function result that returns the data to be observed.
  • callbackA callback when the data to be observed changes, either a function or an object.
  • optionsAdditional options such as:deepTo make a deep observation of the value of the data,immediateIndicates that the callback function is immediately triggered with the value of the observed data.

In createWatcher, handler is a callback function

if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
}
if (typeof handler === 'string') {
    handler = vm[handler];
}
Copy the code

If (typeof Handler === ‘string’); if (typeof handler === ‘string’); So handler is a method in the methods option.

Here’s how Vue implements the VM.$watch instance method.

Vue.prototype.$watch = function(expOrFn, cb, options) { var vm = this; if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {}; options.user = true; var watcher = new Watcher(vm, expOrFn, cb, options); if (options.immediate) { try { cb.call(vm, watcher.value); } catch (error) { handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\"")); } } return function unwatchFn() { watcher.teardown(); }}Copy the code

If (isPlainObject(cb)) : if (isPlainObject(cb)) : if (isPlainObject(cb)) : if (isPlainObject(cb)) : Therefore, run createWatcher(VM, expOrFn, cb, options) to create user Watcher and return it.

Perform the options = options | | {}; options.user = true; Work with the options parameter and add the attribute user and set it to true.

Exe var watcher = new watcher (vm, expOrFn, cb, options), instantiate a class called watcher.

Look at the Watcher constructor. Let’s simplify this by keeping only instance objects related to User Watcher and Dep.

var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) { this.vm = vm; vm._watchers.push(this); if (options) { this.deep = !! options.deep; this.user = !! options.user; } else { this.deep = this.user = false; } this.cb = cb; this.id = ++uid; this.active = true; this.deps = []; this.newDeps = []; this.depIds = new Set(); this.newDepIds = new Set(); this.expression = expOrFn.toString(); if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); if (! this.getter) { this.getter = noop; warn( "Failed watching path: \"" + expOrFn + "\" " + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ); } } this.value = this.lazy ? undefined : this.get(); }Copy the code

Execute VM._watcher. push(this) to cache Watcher, vm._watchers is a set of Watcher.

If {user: true}, this. User =!! Options. User, so the Watcher instance object user is true, which is the flag for user Watcher.

The value of the parameter expOrFn means the data to be monitored, or the publisher to subscribe to. As described in the presentation of render Subscribers, the value of the function is a string, so here’s how to deal with it.

When the parameter expOrFn is a string, it stands for the path of the data, which needs to be parsePath to parse to get the data. Take a look at the parsePath method.

var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); function parsePath(path) { if (bailRE.test(path)) { return } var segments = path.split('.'); return function(obj) { for (var i = 0; i < segments.length; i++) { if (! obj) { return } obj = obj[segments[i]]; } return obj } }Copy the code

For example, path is A.B.C, segments is [a,b,c], and returns a function assigned to this.getter.

Finally, execute this.value = this.lazy? undefined : This.get (), because there is no lazy property in the options parameter of the Watcher constructor in this scenario, so this.lazy is false and this.get() is executed, Value = this.getter.call(VM, VM) is executed in the GET instance method. The this.getter in the scenario is generated by the parsePath function, and its value is shown below

function(obj) { for (var i = 0; i < [a, b, c].length; i++) { if (! obj) { return } obj = obj[[a, b, c][i]]; } return obj }Copy the code

So the argument obj is vm, which is this instantiated object of class Vue. Loop obj = obj[[a, b, c][I]] through [a, b, c]

  • obj = this.a
  • obj = this.a.b
  • obj = this.a.b.c

A, this.a.b, and this.a.b.c will trigger the getter of the data’s descriptor property in which the user-defined subscriber is collected. The collection logic is the same as the collection of rendering subscribers, which will not be repeated. You can see that the core process of collecting user-defined subscribers is basically the same as collecting rendered subscribers.

2. Immediate callback implementation

Back in the vm.$watch instance method, there is the following logic.

if (options.immediate) { try { cb.call(vm, watcher.value); } catch (error) { handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\"")); }}Copy the code

If options has the property immediate and the value is true, run cb. Call (VM, watcher.value). This is the implementation logic of the immediate callback. Watcher. value is the value of the data to be listened on, which is determined by watcher’s get instance method.

3. Cancel the implementation of the listening function

Back in the vm.$watch instance method, there is the following logic.

return function unwatchFn() {
    watcher.teardown();
}
Copy the code

Watcher.teardown () is the key to getting rid of the listener. Teardown is an instance method of watcher.

Watcher.prototype.teardown = function teardown() {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
          remove(this.vm._watchers, this);
      }
      var i = this.deps.length;
      while (i--) {
           this.deps[i].removeSub(this);
      }
      this.active = false;
    }
};
Copy the code

To cancel the listener, in other words to remove the subscriber, we do two things in the tearDown instance method:

  • Remove the subscriber from the global Wacther set this.vm._watchers.

    Execute this.active. If the value of this.active is true, the subscriber is not removed.

    Perform the if (! This.vm. _isBeingDestroyed), vm._isBeingDestroyed is an indication of whether the Vue instance has been destroyed. True indicates that the instance is destroyed. If false, execute remove(this.vm._watchers, this), where this.vm._watchers is the Vue instance object _watchers to save the set of how many subscribers are in the current Vue instance. The remove function removes an item from an array.

    function remove(arr, item) {
        if (arr.length) {
            var index = arr.indexOf(item);
            if (index > -1) {
                return arr.splice(index, 1)
            }
        }
    }
    Copy the code
  • Remove the subscriber from the publishers to which the subscriber subscribers.

    This.deps is a collection that Watcher subscribers use to keep their subscriptions to those publishers, Traversing this.deps executes this.deps[I]. RemoveSub (this) notifies each publisher to call Dep’s instance method removeSub to remove the subscriber.

4. Implementation of deep listening

In the Watcher instance method get to implement the deep listening function, its main logic is as follows

if (this.deep) {
    traverse(value);
}
Copy the code

If the options attribute deep is true, then this. Deep is true, then traverse(value) is executed. Traverse functions perform deep recursive traversal of an object, since traversal is a visit to a child object. The getter function of the child object is fired to collect subscribers, thus implementing a deep listen. Take a look at the traverse function.

var seenObjects = new Set();

function traverse(val) {
    _traverse(val, seenObjects);
    seenObjects.clear();
}

function _traverse(val, seen) {
    var i, keys;
    var isA = Array.isArray(val);
    if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
        return
    }
    if (val.__ob__) {
        var depId = val.__ob__.dep.id;
        if (seen.has(depId)) {
            return
        }
        seen.add(depId);
    }
    if (isA) {
        i = val.length;
        while (i--) {
            _traverse(val[i], seen);
        }
    } else {
        keys = Object.keys(val);
        i = keys.length;
        while (i--) {
            _traverse(val[keys[i]], seen);
        }
    }
}
Copy the code

Perform the if ((! isA && ! IsObject (val) | | Object. IsFrozen (val) | | val instanceof VNode), if the val not arrays and objects, or val is a frozen objects, or VNode class instantiation objects. This is true because val has no more children or children worth traversing to trigger collection subscribers for its children and to trigger collection subscribers for itself by fetching val.

Var depId = val.__ob__.dep.id var depId = val.__ob__.dep. Id var depId = val.__ob__.dep. Dep = new dep (); dep = new Observer(); dep = new Observer(); Def (value, ‘__ob__’, this) this assigns dep to the _ob_ attribute of the data, as described in this column.

Run the if (seen. Has (depId)) command to check whether the parameter seen has depId. If there is a direct return, do not run the command to add the parameter seen. This logic is an optimization to avoid repeated traversal of child objects triggering collection of subscribers. The value of the seen argument is assigned via var seenObjects = new Set(), which is a Set data structure.

Perform if (isA) to determine if val is an array. If so, loop through the data, recursing to _traverse(val[I], seen) for each item in the array and triggering its own collection of subscribers by fetching val[I].

If (val = object.keys (val)), run keys = object.keys (val), then run _traverse(val[keys[I]], seen), It also triggers its own subscriber collection by fetching val[keys[I]].

After the _traverse recursive call is complete, execute seenobjects.clear () to clear any dep.id saved in the process.

Collection of subscribers of computed Watcher

The process of collecting computed attributes subscribers is different from that of collecting other subscribers, and computed attributes also implement caching.

The computed property computed is initialized by executing if (opts.computed) initComputed(VM, opts.computed) in the initState function in the Vue instance initialization. Look at the initComputed function.

var computedWatcherOptions = { lazy: true }; function initComputed(vm, computed) { var watchers = vm._computedWatchers = Object.create(null); var isSSR = isServerRendering(); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; if (getter == null) { warn( ("Getter is missing for computed property \"" + key + "\"."), vm ); } if (! isSSR) { watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions); } if (! (key in vm)) { defineComputed(vm, key, userDef); } else { if (key in vm.$data) { warn(("The computed property \"" + key + "\" is already defined in data."), vm); } else if (vm.$options.props && key in vm.$options.props) { warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); }}}}Copy the code

Execute var watchers = vm._computedWatchers = object. create(null) to create an empty Object Vm. _computedWatchers to store the calculated attribute Watcher.

On computed, assign each computed attribute to userDef, and run var getter = typeof userDef === ‘function’? userDef : userDef.get; Because computed is described in the official website documentation in two ways

ADouble: function() {return this.a * 2}, // Read and set aPlus: {get: function() { return this.a + 1 }, set: function(v) { this.a = v - 1 } } }Copy the code

So if userDef is a function, assign it to the getter. If it’s not a function, then it’s an object that assigns userDef.get to the getter. The getter here will be the value of the expOrFn parameter of the Watcher constructor, also known as an expression that evaluates properties.

Perform the if (! IsSSR) under the rendering scene not server in execution, which [key] = new Watcher (vm, getter | | it, it, computedWatcherOptions), Create a Watcher for the computed properties and add it to vm._computedWatchers. Note that the Watcher constructor argument cb is null noop, and the options argument is {lazy: true}. Look at the Watcher constructor again, and let’s simplify it by keeping only instance objects associated with computed Watcher and Dep.

var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) { this.vm = vm; vm._watchers.push(this); if (options) { this.lazy = !! options.lazy; } this.cb = cb; this.id = ++uid$2; this.active = true; this.dirty = this.lazy; this.deps = []; this.newDeps = []; this.depIds = new _Set(); this.newDepIds = new _Set(); this.expression = expOrFn.toString(); if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); } this.value = this.lazy ? undefined : this.get(); };Copy the code

Value = this.lazy? Use of undefined: this.get(), where the options parameter is {lazy: If the value of this.lazy is true, then this.get() cannot be executed. In the process of collecting rendering subscribers and customizers, the getters that trigger the data in the get instance method are collected. So where is the computational data being collected by subscribers?

Back in the initComputed function, execute if (! (Key in VM)) Determines whether the key value of the computed attribute is defined in the Vue. If not, then defineComputed(VM, key, userDef) is executed. If so, determine whether the key of the computed property is occupied by the data or prop key, and if so, report a warning in the development environment. Let’s take a look at the defineComputed function.

var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function defineComputed(target, key, userDef) { var shouldCache = ! isServerRendering(); if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef); sharedPropertyDefinition.set = noop; } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! == false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop; sharedPropertyDefinition.set = userDef.set || noop; } if (sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function() { warn( ("Computed property \""  + key + "\" was assigned to but it has no setter."), this ); }; } Object.defineProperty(target, key, sharedPropertyDefinition); }Copy the code

DefineComputed is a function that uses object.defineProperty to add a getter and setter to the evaluated property. The setter is usually available when the evaluated property is an Object and has a set method, otherwise it is an empty function. In a normal development scenario, where there are fewer setters for computed properties, let’s focus on the getter part.

Var shouldCache =! isServerRendering(); If userDef is a function, then shouldCache should be false. If userDef is an object, then createComputedGetter(key) should be used. Userdef. cache is deprecated and defaults to true in Vue2, so the createComputedGetter(key) is still executed. Look at the createComputedGetter function.

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

The createComputedGetter function returns a getter function. This getter function fires whenever a evaluated property is used.

Var watcher = this._computedwatchers && this._computedwatchers [key], This._computedWatchers stores the Wacther object set created for each calculated attribute. The key is the calculated attribute. Find the corresponding Wacther and assign it to the constant watcher.

If watcher exists, execute if (watcher.dirty), which is assigned to the Watcher instance by executing this.dirty = this.lazy in the watcher constructor. So the first time the getter is executed, watcher.dirty is true, and watcher.evaluate() is executed. Take a look at evaluate the Watcher instance method.

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

Get (). In get, the Wacther instance method executes the expression that evaluates the property. During execution, it accesses the data in the expression and triggers the getter function for the data. Start collecting compute attribute subscribers, and the collection process is exactly the same as the collection process for rendering subscribers. This.get () returns a value that is assigned to this.value and sets this.dirty to false. The next time the attribute is evaluated, because this.dirty is false, Instead of executing watcher.evaluate(), watcher.value is returned directly, and this is the implementation logic for the cache function that evaluates attributes. When the data in the evaluated attribute expression changes, the evaluated attribute Watcher is told to set this.dirty to true. When the evaluated attribute is used again, watcher.evaluate() is performed, and the evaluated attribute expression is executed again, returning the new value of the evaluated attribute. More on this in a future column.

Execute if (dep.target) {watcher.depend()}. Because after the calculated attribute subscriber is collected in this.get(), popTarget() is executed to get the last subscriber (rendering subscriber) off the stack and then assign to dep.target. So dep. target is the render subscriber at this point.

Take a look at the watcher.depend instance method

Watcher.prototype.depend = function depend() {
    var i = this.deps.length;
    while (i--) {
        this.deps[i].depend(); }};Copy the code

Here’s an example. For example, the computation attribute A is defined as follows.

<template>
    <div>{{a}}</div>
</template>
<script>
export default {
    data(){
        return{
            b:1,
            c:2
        }
    },
    computed:{
        a:function(){
            return  this.b + this.c
        }
    }
}
</script>
Copy the code

Then how to collect the render subscribers when the render template reads the computed property a. It is this.b and this.c that make up the value of the computed property that collect the render subscriber. This is because the computed attribute A is a change caused by a change in this.b or this.c.

Since this.b or this.c first collected the computed attribute subscribers, the this.deps of the computed attribute subscribers includes the dependency collector of this.b or this.c. The traversal executes this.deps[I].depend(), triggering this.b and this.c to collect render subscribers.

Four, subsequent

This article detailed the collection process for three types of subscribers, and the next column is prepared to cover how to notify subscribers to perform updates when a publisher changes. In addition, Watcher will also introduce many instance objects and instance usage. Look forward to it. thank you