To get into the inner workings of a responsive system, we devoted an entire section to how data (including Data, computed, and props) is initialized as responsive objects. With the knowledge of responsive data objects, we built a responsive system using data as data in the second half of the previous section on the basis of retaining the source code structure. In this section, we continue to dig into the details of the internal construction of a responsive system and analyze in detail Vue’s processing of data and computed in a responsive system.

7.8 Related Concepts

In building simple responsive systems, we have introduced several important concepts that are at the heart of responsive principle design. Let’s briefly review them:

  • ObserverClass, instantiate oneObserverClass will passObject.definePropertyOf the datagetter,setterMethod to rewrite ingetterphasesCollection of dependenciesIn the data update phase, triggersettermethodsDependent update
  • watcherClass, instantiatedwatcherA class creates a dependency, and the simple understanding is that a dependency is created where the data is used. Each dependency is notified to update when the data changes, as mentioned earlier in renderingwathcerIs renderingdomWhen using data generated dependencies.
  • DepClass, sincewatcherUnderstood as the dependencies that each piece of data needs to listen to, the collection and notification of those dependencies needs to be managed by another class, this oneDep.DepThere are only two things you need to do: collect dependencies and distribute update dependencies.

These are the three basic core concepts of responsive system construction and are the basis of this section. If you are not impressed, please review the previous section on minimalist wind responsive system construction.

7.9 the data

7.9.1 Question thinking

Before we start to analyze the data, we will throw out a few questions for the reader to think about, and the answers will be included in the following content analysis.

  • We already know that the Dep is used as a container for managing dependencies, so when is this container generated? So when does the instantiation Dep happen?

  • What types of dependencies does THE Dep collect? What are Watcher’s categories of dependencies, what are the scenarios, and what are the differences?

  • What exactly does the Observer class do with getter,setter methods?

  • If both the hand-written Watcher and the page data rendering watch are listening for changes in data, what is the priority?

  • With the collection of dependence, is there still dependency relief, dependency relief in the meaning of where?

With these questions in mind, we begin to analyze the responsive details of Data.

7.9.2 Dependency Collection

During initialization, data instantiates an Observer class defined as follows (ignoring data of array type):

// initData 
function initData(data) {
  ···
  observe(data, true)}// observe
function observe(value, asRootData) {... ob =new Observer(value);
  return ob
}

Getter and setter methods are overridden for all properties of the object as long as the object is set to have an observer property. Getter and setter methods collect and distribute dependent updates for all properties of the object
var Observer = function Observer (value) {...// Set the __ob__ property to non-enumerable. External cannot be obtained by traversal.
    def(value, '__ob__'.this);
    // Array processing
    if (Array.isArray(value)) {
        ···
    } else {
      // Object processing
      this.walk(value); }};function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable:!!!!! enumerable,// Whether enumerable
    writable: true.configurable: true
  });
}
Copy the code

The Observer adds an __ob__ attribute to data. The __ob__ attribute is used as an indication of a responsive object, and the def method ensures that the attribute is non-enumerable, meaning that it cannot be iterated to obtain its value. In addition to marking reactive objects, the Observer class also calls the walk method on the prototype, iterating through getter and setter overrides for each property on the object.

Observer.prototype. Walk = function walk (obj) {var keys = object.keys (obj); for (var i = 0; i < keys.length; i++) { defineReactive###1(obj, keys[i]); }};Copy the code

DefineReactive# ##1 is the core of reactive build. It instantiates a Dep class, creating a dependent management for each data, and then overrides getter and setter methods using object.defineproperty. Here we only analyze code that depends on the collection.

function defineReactive# # # 1 (obj,key,val,customSetter,shallow) {
    // Create a dependency management by instantiating one Dep class per data
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    // Attributes must be configurable
    if (property && property.configurable === false) {
      return
    }
    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    // This part of the logic is for deep objects. If the object's property is an object, the recursive call instantiates the Observe class to convert its property value to a responsive object
    varchildOb = ! shallow && observe(val);Object.defineProperty(obj, key, {
      enumerable: true.configurable: true,s
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          // Add deP data for the current watcher
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) { dependArray(value); }}}return value
      },
      set: function reactiveSetter (newVal) {}}); }Copy the code

We know that when a property value in data is accessed, it will be intercepted by the getter function. According to our old knowledge system, we can know that the instance will create a rendering watcher before mounting.

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

At the same time, the updateComponent logic performs the instance mount, during which the template is parsed first into the Render function, which, when converted to Vnode, accesses the defined data, triggering gettter for dependency collection. The data collection relies on the render Watcher itself.

The dependency collection phase in your code does several things:

  1. Add owned data for the current Watcher (in this case, the render Watcher).
  2. Dependencies that need to be listened on for the current data collection

How to understand these two points? Let’s look at the implementation in the code first. The getter phase executes dep.depend(), which is the method dep class defines on the prototype.

dep.depend();


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

Target is the currently executing watcher. In the render phase, dep. target is the render Watcher instantiated when the component is mounted, so the Depend method will call the addDep method of the current Watcher to add dependent data to the Watcher.

Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      // newDepIds and newDeps record the data owned by Watcher
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      // Avoid adding the same data collector repeatedly
      if (!this.depIds.has(id)) {
        dep.addSub(this); }}};Copy the code

Where newDepIds are data structures with unique members of Set and newDeps are arrays, they are used to record the data currently owned by watcher. This process will make logical judgment to avoid adding the same data more than once.

AddSub adds the Watcher that needs to be listened on for each data-dependent collector.

Dep.prototype.addSub = function addSub (sub) {
  // Add the current watcher to the data dependency collector
    this.subs.push(sub);
};
Copy the code
  1. getterIf an object is encountered with an attribute value, dependencies are collected for each value of that object

If we change a basic type of reactive data into an object, the properties in the new object must also be set to reactive data.

  1. We do special processing when we encounter an array of property values, which we’ll talk about later.

To summarise the dependency collection process in a layman’s way, each data is a dependency manager, and each place where data is used is a dependency. When data is accessed, the currently accessed scenario is collected as a dependency into the dependency manager, as well as the owned data for the dependencies for that scenario.

7.9.3 Sending updates

In analyzing a dependency collection, there may be some confusion as to why so many relationships should be maintained. What role do these relationships play when the data is updated? With that in mind, let’s take a look at the process of distributing updates. When the data changes, the defined setter methods are executed, so let’s look at the source code.

ObjectDefineProperty (obj, key, {... set:function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      // The new value is equal to the old value
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return}...// When the new value is an object, the dependency collection process is performed for the new object
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
})
Copy the code

The distribution phase does the following:

  • Check whether the data changes are consistent. If the data changes are consistent, no update operation is performed.
  • When the new value is an object, the dependency collection process is performed on the properties of that value.
  • Notifying the data collectionwatcherDependency, iterating through eachwatcherUpdate dataThis is the phase that invokes the data-dependent collectordep.notifyMethod for the distribution of updates.
Dep.prototype.notify = function notify () {
    var subs = this.subs.slice();
    if(! config.async) {// Sort by dependent id
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      // Iterate over each dependency to update the data.subs[i].update(); }};Copy the code
  • Each will be updated whenwatcherPush into the queue and wait for the next onetickTake each out when it arriveswatcherforrunoperation
 Watcher.prototype.update = function update () {
    ···
    queueWatcher(this);
  };
Copy the code

The call to the queueWatcher method pushes the dependencies collected by the data in turn into the Queue array, which updates the view based on the cached results in the next event loop ‘TICK’. When performing a view update, it is inevitable that new dependencies will be added to the render template due to data changes, which in turn will perform queueWatcher procedures. So you need a flag bit to record whether you are in the queue for an asynchronous update process. This flag bit is flushing, so when we do an asynchronous update, the new watcher is inserted into the queue.

function queueWatcher (watcher) {
    var id = watcher.id;
    // Execute the same watcher only once
    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); }... nextTick (flushSchedulerQueue); }}Copy the code

The principle and implementation of nextTick will not be discussed. Generally speaking, nextTick will buffer multiple data processing processes and then execute DOM operations in the next event cycle TICK. The principle of nextTick is to realize asynchronous update by using the microtask queue of the event cycle.

When the next tick arrives, the flushSchedulerQueue method is executed, which takes the collected queue array (which is a collection of Watchers) and sorts the array dependencies. Why do I sort? The source code explains three points:

  • Component creation is parent, so component updates are parent, so parent rendering needs to be guaranteedwatcherPrecedence over child renderingwatcherThe update.
  • The user – defined Watcher is called user Watcher. User Watcher and Render Watcher are implemented first. Since User Watchers was created before Render Watcher, user Watcher is implemented first.
  • If a component is in the parent component’swatcherThe execution phase is destroyed, then it corresponds towatcherExecution can be skipped.
function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();
    flushing = true;
    var watcher, id;
    // Sort the watcher of the queue
    queue.sort(function (a, b) { return a.id - b.id; });
    // Loop through queue.length to ensure that the length of queue changes due to new dependencies being added at render time.
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index];
      // If watcher defines the before configuration, the before method takes precedence
      if (watcher.before) {
        watcher.before();
      }
      id = watcher.id;
      has[id] = null;
      watcher.run();
      // in dev build, check and stop circular updates.
      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();
    // Reset the recovery state and empty the queue
    resetSchedulerState();

    // Call other hooks after view changes
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);

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

In flushSchedulerQueue, four important processes can be summarized as follows:

  • rightqueueIn thewatcherFor reasons summarized above.
  • traversewatcher, if the currentwatcherThere arebeforeIf yes, run the commandbeforeMethod corresponding to the previous renderwatcher: the renderingwatcherWhen we instantiate, we passbeforeThe function, which is the next onetickIs called before updating the viewbeforeUpdateLifecycle hooks.
  • performwatcher.runModify operations.
  • Reset the recovery state, which restores some process-controlled state variables to their initial values and clears recordswatcherIn the queue.
new Watcher(vm, updateComponent, noop, {
  before: function before () {
    if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate'); }}},true /* isRenderWatcher */);
Copy the code

Focus on the operation watcher.run().

Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      if( value ! = =this.value || isObject(value) || this.deep ) {
        // Set the new value
        var oldValue = this.value;
        this.value = value;
        // For user watcher, do not analyze for now
        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

The new value will be evaluated. If the new value meets the criteria,cb will be executed. Cb is the callback passed in when watcher is instantiated.

Before looking at the GET method, let’s look back at a few property definitions of the Watcher constructor

var watcher = function Watcher(
  vm, //Component instance expOrFn,//I'm going to do cb,//The callback options,//Configuration isRenderWatcher//Whether to render watcher) {
  this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !! options.deep;this.user = !! options.user;this.lazy = !! options.lazy;this.sync = !! options.sync;this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    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 ); }}// Lazy is the evaluation attribute flag. When watcher is the evaluation watcher, it is not understood to execute the get method to evaluate
    this.value = this.lazy
      ? undefined
      : this.get();
  
}
Copy the code

The get method is defined as follows:

Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch(e) {...}finally{...// Restore dep. target to its previous state, depending on the collection process
      popTarget();
      this.cleanupDeps();
    }
    return value
  };
Copy the code

The get method evaluates this. Getter, which performs view updates under the current render watcher condition. This stage rerenders the page components

new Watcher(vm, updateComponent, noop, { before: (a)= >{}},true);

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};
Copy the code

After the getter method is executed, the last step is to cleanup the dependency, which is cleanupDeps.

Here’s a scenario for dependency cleanup: We often use the v – if for template switch, the switch can perform different template in the process of rendering, if A template to monitor data, A template to monitor data, B B when rendering the template B, without relying on old removal, in B template scenarios, the change of the data can also cause A dependent to apply colours to A drawing update, which can cause the performance of the waste. Therefore, the removal of old dependencies is necessary during the optimization phase.

// Depend on the cleanup process
  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

Summarize the above analysis into the last two points that rely on distributed updates

  • performrunThe action will be executedgetterMethod, that is, recalculate the new value, for renderingwatcherIs re-executedupdateComponentUpdate the view
  • recalculategetterAfter, the dependency is cleared

7.10 the computed

Calculated attributes are designed for simple calculations, because putting too much logic into a template can make it too heavy and difficult to maintain. For computed analysis, we still follow the two processes of dependent collection and distributed update.

7.10.1 Relying on Collection

Initialization of computed iterates through every attribute value of computed and instantiates a computed Watcher for each attribute, with {lazy: True} is a symbol for computed Watcher, and you end up calling defineComputed to set the data to reactive, with the source code as follows:

function initComputed() {...for(var key in computed) {
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
  }
  if(! (keyinvm)) { defineComputed(vm, key, userDef); }}For computed Watcher, the lazy attribute is true
var computedWatcherOptions = { lazy: true };
Copy the code

The logic of defineComputed is similar to that of analyzing data, and eventually object.defineProperty is called for data interception. Specific definitions are as follows:

function defineComputed (target,key,userDef) {
  // Non-server rendering caches the getter
  varshouldCache = ! 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

The situation in the rendering of a service, calculation results will be cached attributes, the sense of the cache is that only in the relevant responsive data changes, the computed to evaluate, the rest of the multiple access attribute’s value will be calculated before returning the result of calculation, this is the cache optimization, computed attribute has two kinds of writing, One is a function, and the other is an object, which is written to provide getters and setters.

When a computed property is accessed, a getter method is triggered for dependency collection. Look at the implementation of 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
      }
    }
  }
Copy the code

The function returned by the createComputedGetter gets a computed watcher attribute first during execution. Dirty indicates whether a calculation has been performed, and watcher. Evaluate does not evaluate, which is how caching works.

Watcher.prototype.evaluate = function evaluate () {
    Evaluate is used to perform the evaluate callback for the evaluate attribute
    this.value = this.get();
    this.dirty = false;
  };
Copy the code

The get method, as described earlier, calls the execution function passed when the Watcher is instantiated. In the case of the Computer Watcher, the execution function is the calculation function that evaluates the property. It can be either a function or a getter method for an object.

Enumerating a scenario to avoid disconnection with data processing. Computed In the computing phase, if an attribute value of data is accessed, the getter method of data data is triggered for dependency collection. According to the previous analysis, the Dep collector of data collects the current Watcher as a dependency. This watcher is computed Watcher, and the accessed data Dep is added to the current watcher

Return to the this.get() method of calculating the execution function. After the getter is executed, the dependency will also be cleared. The principle and purpose refer to the analysis of the data stage. After get is executed, it enters Watcher. Depend for dependency collection. The collection process is consistent with data, with the current computed Watcher collected as a dependency collector in THE Dep.

This is the whole process of computed dependent collection, which caches the results of operations and avoids repeated operations, as opposed to data dependent collection.

7.10.2 Sending updates

The condition for distributing updates is that the data in the data has changed, so most of the logics are consistent with the data analysis. Let’s make a summary.

  • When the calculated attribute dependency data is updated, due to the dataDepcollectcomputed watchThis dependency, so will be calleddepthenotifyMethod to update the status of dependencies.
  • At this timecomputed watcherAnd what we introduced beforewatcherInstead, it does not perform the dependent update operation immediately, but through onedirtyMark it. Let’s go backDepend on the updateThe code.
Dep.prototype.notify = function() {...for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
}

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

Due to the lazy attribute, the update procedure does not perform a status update and only marks dirty as true.

  • Due to thedataData ownership renderingwatcherThis dependency is executed at the same timeupdateComponentTo re-render the view, whilerenderProcedure will access the calculated properties, at this point due tothis.dirtyA value oftrue, and the evaluated property is reevaluated.

7.11 summary

On the theoretical basis of the previous section, we analyzed in depth how Vue uses data and computed to build responsive systems. The core of the responsive system is to intercept the getter and setter of the data by using Object.defineProperty. The core of the processing is to collect the dependencies of the scene where the data is accessed and inform the collected dependencies to update when the data is changed. In this section, we have taken a closer look at responsive processing in data and computed, both of which have very similar but different processing logic. Computed results are cached in the source code to avoid the problem of frequent double-counting when used in multiple places. Due to space constraints, we’ll leave the analysis of user – defined Watcher to the next section. There is still a puzzle left in this article, which will be solved one by one in the future.


  • An in-depth analysis of Vue source code – option merge (1)
  • An in-depth analysis of Vue source code – option merge (2)
  • In-depth analysis of Vue source code – data agents, associated child and parent components
  • In-depth analysis of Vue source code – instance mount, compile process
  • In-depth analysis of Vue source code – complete rendering process
  • In-depth analysis of Vue source code – component foundation
  • In-depth analysis of Vue source code – components advanced
  • An in-depth analysis of Vue source code – Responsive System Building (PART 1)
  • In – Depth Analysis of Vue source code – Responsive System Building (Middle)
  • An in-depth analysis of Vue source code – Responsive System Building (Part 2)
  • In-depth analysis of Vue source code – to implement diff algorithm with me!
  • In-depth analysis of Vue source code – reveal Vue event mechanism
  • In-depth analysis of Vue source code – Vue slot, you want to know all here!
  • In-depth analysis of Vue source code – Do you understand the SYNTAX of V-Model sugar?
  • In-depth analysis of Vue source – Vue dynamic component concept, you will be confused?
  • Thoroughly understand the keep-alive magic in Vue (part 1)
  • Thoroughly understand the keep-alive magic in Vue (2)