In the last section, we looked in depth at the process of creating a responsive system for data using Data,computed, and the process of relying on the collection and distribution of updates. However, there are still more or less problems in the process of use and analysis. In this section, we will analyze these problems. Finally, we will also analyze the responsive process of Watch. This article will be the conclusion of responsive systems analysis.

7.12 Array Detection

Earlier in the data broker chapter, We have introduced a Vue agent technology is to use the data Object. The defineProperty, Object, defineProperty let we can use a convenient access descriptor of the getter/setter for data monitoring, the get, do not set the hook The same operation, to achieve the purpose of data interception. DefineProperty’s get and set methods, however, can only detect changes in Object attributes. For array changes (such as inserting or deleting array elements), Object.defineproperty cannot achieve the purpose, which is also the defect of using Object.defineProperty for data monitoring. Although the proxy in ES6 can solve this problem perfectly, there are compatibility problems after all. So we also need to investigate how Vue listens on arrays based on Object.defineProperty.

7.12.1 Overriding array methods

Since arrays can no longer listen for changes through getters and setters of data, Vue overwrites array methods to perform additional operations on arrays while preserving the functionality of the original array. That’s redefining the array method.

var arrayProto = Array.prototype;
// Create an object that inherits from Array
var arrayMethods = Object.create(arrayProto);

// The method owned by the array
var methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
];
Copy the code

ArrayMethods is an object class inherited from the original Array class. Due to the inheritance of the prototype chain, arrayMethod has all the methods of the Array. Then, the methods of the new Array class are rewritten.

methodsToPatch.forEach(function (method) {
  // The method to buffer the original array
  var original = arrayProto[method];
  // Rewrite the execution of the method with object.defineProperty
  def(arrayMethods, method, function mutator () {});
});

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable:!!!!! enumerable,writable: true.configurable: true
    });
  }

Copy the code

When arrayMethods is executed, the mutator function is executed by proxy. The implementation of this function is described in the distributive update of arrays.

It’s not enough to just create a new collection of array methods. How do we access an array without calling the native array methods, and instead point the procedure to the new class, is the focus of the next step.

Going back to the data initialization process, which is executing initData, we spent a lot of time in the last article describing how data initialization creates an Observer class for data. We only showed that the Observer class intercepts data for each non-array property. Redefine getters, setters, and other than that, we intentionally skipped the analysis for array types. Here, we’ll focus on handling array intercepts.

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  // Set the __ob__ property to non-enumerable. External cannot be obtained by traversal.
  def(value, '__ob__'.this);
  // Array processing
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
  // Object processing
    this.walk(value); }}Copy the code

The array processing branch is divided into two, hasProto criteria, hasProto is used to determine whether the __proto__ attribute is supported in the current environment. And the array processing will perform protoAugment, copyAugment depending on whether this property is supported or not,

// The judgment of the __proto__ attribute
var hasProto = '__proto__' in {};
Copy the code

When the support__proto__When performingprotoAugmentPoints the prototype of the current array to the new array classarrayMethodsIf not supported__proto__, the proxy is set to access the array methods in the new array class when the array methods are accessed.

// Directly through the prototype pointing

function protoAugment (target, src) {
  target.__proto__ = src;
}

// Through the data proxy
function copyAugment (target, src, keys) {
  for (var i = 0, l = keys.length; i < l; i++) {
    varkey = keys[i]; def(target, key, src[key]); }}Copy the code

With these two steps, we then execute the arrayMethods class when we call the arrayMethods like push, unshift, etc., inside the instance. This is also a prerequisite for arrays to rely on collecting and distributing updates.

7.12.2 Dependency Collection

Since the data initialization phase uses Object.definePrototype to override data access, array access is also intercepted by the getter. This is an array, the interception process will do a special dependArray.

function defineReactive# # # 1 () {...varchildOb = ! 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() {}
}
 
Copy the code

ChildOb indicates whether the value of an attribute is of a basic type. Observe returns a basic type without processing it. If it encounters an object or array, it recursively instantiates an Observer, setting responsive data for each child attribute, and finally returns an Observer instance. Instantiating an Observer returns to the old process of adding an __ob__ attribute, prototype repointing if an array is encountered, and defining getters and setters if an object is encountered. This process has been discussed before and will not be described.

When an array is accessed, childob.dep.depend () is executed because of childOb; Dependence. the DEP attribute of the Observer instance collects the current Watcher as a dependence. a dependArray ensures that if the array element is an array or object, dependencies are recursively collected for the internal element.

function dependArray (value) {
    for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
      e = value[i];
      e && e.__ob__ && e.__ob__.dep.depend();
      if (Array.isArray(e)) { dependArray(e); }}}Copy the code

We can take a screenshot to see the result of the final dependency collection.

Before collecting

After collection

7.12.3 Sending updates

When we call array methods to add or remove data, setter methods of data can’t intercept, so the only way we can intercept is when we call array methods, as we saw earlier, Calls to arrayMethods are propped into methods of the new class arrayMethods, whose arrayMethods are overridden. Let’s look at his definition.

 methodsToPatch.forEach(function (method) {
    var original = arrayProto[method];
    def(arrayMethods, method, function mutator () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];
      // Execute the primitive array method
      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      if (inserted) { ob.observeArray(inserted); }
      // notify change
      ob.dep.notify();
      return result
    });
  });

Copy the code

Mutator is an overridden array method that first calls the original array method. This ensures consistency with the original array type method. Args holds the arguments passed by the array method call. After retrieving the array’s __ob__ (the previously saved Observer instance), call ob.dep.notify(); Do dependency distribution updates, as you know. The DEP of an Observer instance is an instance of deP, which collects watcher dependencies that need to be listened on, and notify recalculates and updates the dependencies. Prototype. Notify = function notify () {}

Back in the code, the INSERTED variable is used to indicate whether an element has been added to the array, and if the added element is not of the original type, but of the array object type, the observeArray method needs to be triggered to do a dependency collection on each element.

Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); }};Copy the code

In general. Array changes are not triggeredsetterDo dependency updates, soVueCreates a new array class, overrides the array methods and points them to the new array class. It also fires when an array is accessedgetterPerforms dependency collection. When an array is changed, new array method operations are triggered and dependencies are distributed.

Now let’s go back to the official Vue documentation for a note on array detection:

Vue cannot detect changes to the following arrays:

  • When you set an array item directly using an index, for example:vm.items[indexOfItem] = newValue
  • When you modify the length of an array, for example:vm.items.length = newLength

Obviously, given the above analysis, it is easy to understand the drawbacks of array detection. Even though Vue rewrites array methods to intercept when setting arrays, it cannot trigger dependent updates, either by indexing or directly changing the length.

7.13 Object Detection Is Abnormal

In real development, we often encounter a scenario where the object test: {a: 1} is adding an attribute B. If we use test.b = 2 to add an attribute B, this process will not be detected by Vue, and the reason is very simple. When we collect dependencies on an object, we collect dependencies for each attribute of the object. New attributes added directly through test.b do not rely on the collection process, so they will not be updated when data B changes later.

Set (object, propertyName, value) and Vue. $set(object, propertyName, value).

Vue.set = set function set (target, key, Val) {/ / target must be non-null object if (isUndef (target) | | isPrimitive (target)) {warn ((" always set a reactive property on undefined, null, or primitive value: " + ((target)))); } // Array scenario, call overridden splice method, collect dependencies on newly added properties. if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); If (key in target &&! (key in Object.prototype)) { target[key] = val; Var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { warn( 'Avoid adding reactive properties to a Vue instance or its root $data '  + 'at runtime - declare it upfront in the data option.' ); Return val} // The target source object is not itself a responsive object, so there is no need to handle if (! ob) { target[key] = val; Return val} // Manually call defineReactive,set getter for new property,setter defineReactive###1(ob.value, key, val); ob.dep.notify(); return val }Copy the code

According to the branches, it is divided into four different processing logics:

  1. The target object must be a non-empty object, which can be an array, or an exception will be thrown.
  2. Is called if the target object is an arrayspliceMethod, which is called when encountering a new element in an arrayob.observeArray(inserted)Collect dependencies on elements added to an array.
  3. If the new property value already exists in the original object, the new property value is accessed manually, which triggers dependency collection.
  4. Manually defining new attributesgetter,setterMethod, and throughnotifyTrigger dependency updates.

7.14 nextTick

In the previous section, we said that data changes trigger setter methods for dependent distribution updates, which queue each Watcher until the next tick arrives to perform DOM render updates. This is the asynchronous update process. To illustrate the concept of asynchronous updates, the browser’s event loop mechanism and optimal rendering timing need to be involved. Since this is not the main line of the article, I will use a simple language overview.

7.14.1 Event Loop Mechanism

  1. A complete event loop mechanism requires an understanding of two types of asynchronous queues:macro-taskandmicro-task
  2. macro-taskCommon areSetTimeout, setInterval, setImmediate, Script scripts, I/O operations, UI rendering
  3. micro-taskCommon arepromise, process.nextTick, MutationObserverEtc.
  4. The complete event cycle flow is: 4.1micro-taskEmpty,macro-taskThe queue onlyscriptScript, roll outmacro-taskthescriptTask execution, generated during script executionMacro - task, micro - a taskPush to the corresponding queue 4.2 Execute allmicro-taskIn the microtask event 4.3 executionDOMOperation, render update page 4.4 executionweb workerAnd other related tasks 4.5 cycle, take outmacro-taskTo execute a macro task event, repeat 4.

From the above we can find that in the process, the best rendering process occurred in the process of micro task queue execution, his nearest page rendering process, so we can use micro task queue to implement asynchronous update, it can make the complex operation of batch operation in JS, and view rendering only care about the end result, it greatly reduces the loss of performance.

Here’s an example of the benefits of this approach: Since Vue is a data-driven view update rendering, if we repeatedly evaluate a reactive data in an operation, such as this.num ++ 1000 times in a loop, because of a reactive system, the data changes trigger setters, which trigger relies on issuing updates, The update calls run to re-render the view. This time around, the view renders a thousand times, which is obviously a waste of performance, and we only need to focus on the last thousandth update to the interface. So it’s important to take advantage of asynchronous updates.

7.14.2 Basic Implementation

Vue collects dependent executions with a queue, executes the Run operation of the Watcher in the queue at the next microtask execution, and at the same time, Watcher with the same ID is not added to the queue repeatedly, so it does not repeat the view rendering. Let’s look at the implementation of nextTick.

// The method defined on the prototype
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)};// the method defined on the constructor
Vue.nextTick = nextTick;

// The actual definition
var callbacks = [];
function nextTick (cb, ctx) {
    var _resolve;
    // Callbacks are arrays that maintain microtasks.
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick'); }}else if(_resolve) { _resolve(ctx); }});if(! pending) { pending =true;
      // Push the maintenance queue to the microtask queue for maintenance
      timerFunc();
    }
    NextTick does not pass parameters and the browser supports Promises, a Promise object is returned
    if(! cb &&typeof Promise! = ='undefined') {
      return new Promise(function (resolve) { _resolve = resolve; }}})Copy the code

NextTick is defined as a function that uses vue.nexttick ([callback, context]). When a callback is wrapped by nextTick, the callback will be called in the nextTick. Implementionally, a callbacks is a queue that maintains tasks that need to be executed on the next tick, and each element of it is a function that needs to be executed. Pending is a flag that determines whether a microtask queue is waiting to execute. TimerFunc is the function that really pushes the task queue into the microtask queue. Let’s look at the timerFunc implementation.

1. If the browser executes the Promise, the default is Promsie to push the execution to the microtask queue.

var timerFunc;

if (typeof Promise! = ='undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function () {
    p.then(flushCallbacks);
    // Mobile compatible code
    if(isIOS) { setTimeout(noop); }};// Use the flag of the microtask queue
  isUsingMicroTask = true;
}
Copy the code

FlushCallbacks = flushCallbacks; flushCallbacks = flushCallbacks; flushCallbacks = flushCallbacks; flushCallbacks = flushCallbacks;

function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  // Fetch each task in the Callbacks array and execute the task
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) { copies[i](); }}Copy the code

2. MutataionObserver is supported instead of Promise

else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  }
Copy the code

3. If the microtask method is not supported, the macro task method is used, and setImmediate is used first

 else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Techinically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  }
Copy the code

4. SetTimeout in macro task method will be used when all methods are not suitable

else {
  timerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}
Copy the code

When nextTick does not pass any parameters, it can be used as a promise, for example:

nextTick().then((a)= > {})
Copy the code

7.14.3 Application Scenarios

With all that said, let’s go back to the nextTick usage scenario. Due to the asynchronous update principle, data that changes at one point in time does not trigger a view update. Instead, the view is updated only when the nextTick arrives.

<input v-if="show" type="text" ref="myInput">

// js
data() {
  show: false
},
mounted() {
  this.show = true;
  this.$refs.myInput.focus();/ / an error
}
Copy the code

When data changes, the view does not change at the same time, so you need to use nextTick

mounted() {
  this.show = true;
  this.$nextTick(function() {
    this.$refs.myInput.focus();/ / normal})}Copy the code

7.15 watch

Up to now, most of the analysis of the responsive system has been completed. There is still a problem left in the last section, that is, how Vue can intercept the data of the watch manually added by users. Let’s look at two basic forms of use.

/ / watch options
var vm = new Vue({
  el: '#app',
  data() {
    return {
      num: 12}},watch: {
    num() {}
  }
})
vm.num = 111

// $watch API
vm.$watch('num'.function() {}, {
  deep:,immediate:})Copy the code

7.15.1 Relying on Collection

We analyze the details of watch in terms of the watch option, again starting with initialization. The initialization data will execute initWatch, and the core of initWatch is createWatcher.

function initWatch (vm, watch) {
    for (var key in watch) {
      var handler = watch[key];
      // Handlers can be arrays that perform multiple callbacks
      if (Array.isArray(handler)) {
        for (var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); }}else{ createWatcher(vm, key, handler); }}}function createWatcher (vm,expOrFn,handler,options) {
    // If watch is an object, call the handler in the option
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }
Copy the code

Either in option or API form, the $watch method of the instance is eventually called, where expOrFn is the listened string, handler is the listened callback, and options is the relevant configuration. Let’s focus on the $watch implementation.

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 watch has the immediate option, the cb method is executed immediately, that is, the callback is executed immediately without waiting for attribute changes.
    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

The core of $watch is to create a user watcher,options.user is the current user defined watcher flag. If there is an immediate attribute, the callback function is executed immediately. When watcher is instantiated, a getter evaluation is performed, and the User Watcher is collected as a dependency on the data. This process can refer to the analysis of data.

var Watcher = function Watcher() {...this.value = this.lazy
      ? undefined
      : this.get();
}

Watcher.prototype.get = function get() {...try {
    // Getter callback function to trigger dependency collection
    value = this.getter.call(vm, vm); }}Copy the code

7.15.2 Sending updates

The process by which Watch sends out updates is well understood. Setter interceptors update dependencies when data changes, whereas user Watcher was already collected as a dependency. The dependent update is the execution of the callback function.

7.16 summary

This section is the end of the construction of responsive systems, and how to design responsive systems through data and Computed Tomography, which have been analyzed in detail in the last section. This section analyzes some special scenarios. For example, due to the defect of Object.defineProperty itself, the new and deleted array cannot be intercepted and detected. Therefore, Vue performs special treatment on the array, overwrites the array method, and intercepts the data in the method. We also highlighted how nextTick works, using the browser’s event loop mechanism to achieve optimal rendering timing. At the end of the article, the principle of responsive design of Watch is added. User-defined Watch will create a dependency, which will execute callback when data changes.


  • 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)