Vue2 source code analysis Array change detection

catalogue

1 how to track changes

2 interceptor

3 use interceptors to cover the Array prototype

4 mount the interceptor method on the attribute of the array

5 how to collect dependencies

6 Where does the dependency list exist

7 collection dependency

8 get the Observer instance in the interceptor

9 send notification to array dependencies

10 detect the changes of elements in the array

11 detect changes in new elements

12 questions about Array

13 summary

1 how to track changes

Before es6, js did not provide the ability of meta programming, and pages did not provide the ability to intercept prototype methods, but you can use custom methods to cover the native prototype methods.

You can override the Array with an interceptor prototype. After that, whenever the Array is operated by the method on the Array prototype, the method provided in the interceptor is actually executed, such as the push method. Then, use the prototype method of the native Array to operate the Array in the interceptor.

2 interceptor

The interceptor is actually an array Objects with the same prototype contain exactly the same attributes, but some methods in this Object that can change the contents of the array itself are handled by us.

There are seven methods in the Array prototype that can change the contents of the Array itself, namely push, pop, shift, unshift, splice, sort, reverse;

const arrayProto = Array.prototype;
// Use arrayMethods to overwrite Array.prototype
export const arrayMethods = Object.create(arrayProto);
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    // Cache original method
    const original = arrayProto[method];
    /**
     * Use the Object.defineProperty method on arrayMethods to change the array itself
     * Method of content encapsulation
     */
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        /**
         * When using the push method, it actually uses arrayMethods.push, and arrayMethods.push is the function mutator,
         * Actually, the mutator function is executed.
         * Execute original in mutator to do what it should do, such as push function.
         */
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

3 use interceptors to cover the Array prototype

After having an interceptor, if you want it to take effect, you need to use it to overwrite the Array prototype. However, it cannot be directly covered, which will pollute the global Array. We just want the interceptor to cover only the prototype of the responsive Array.

export class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) {
      /**
       * Its function is to assign the interceptor (arraymethods with interception function after processing) to value__ proto__,
       * Adopted__ proto__ It can skillfully realize the function of covering the value prototype
       */
      value.__proto__ = arrayMethods; // newly added
    } else {
      this.walk(value);
    }
  }
}

4 mount the interceptor method on the attribute of the array

If it cannot be used__ proto__, Directly set these methods on arrayMethods to the detected array;

import { arrayMethods } from "./array";
// __ proto__ Available or not
const hasProto = "__proto__" in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) {
      // modify
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value);
    }
    // ....
  }
}
function protoAugment(target, src, keys) {
  // Overlay prototype
  target.__proto__ = src;
}
function copyAugment(target, src, keys) {
  //   Add the prototype method that has processed the interception operation directly to the attribute of value
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}

5 how to collect dependencies

Object dependencies are collected using Dep in the getter in defineReactive, and each key will have a corresponding Dep list to store dependencies.

Array dependencies, like objects, are also collected in defineReactive:

function defineReactive(data, key, val) {
  if (typeof val === "object") new Observer(val);
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend();
      // Here we collect the dependencies of Array
      return val;
    },
    set: function () {
      if (val === newVal) {
        return;
      }
      dep.notify();
      val = newVal;
    },
  });
}

Therefore, Array collects dependencies in getter s and triggers dependencies in interceptors.

6 Where does the dependency list exist

vue.js stores Array dependencies in Observer:

export class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep(); // Add dep

    if (Array.isArray(value)) {
      // ....
    }
    // ...
  }
}

Why should the dep (dependency) of the array be saved on the Observer instance?

As mentioned earlier, arrays collect dependencies in getters and trigger dependencies in interceptors, so the location of saving dependencies is critical, and it must be accessible in both getters and interceptors.

The reason why we save the dependency on the Observer instance is that the Observer instance can be accessed in the getter and the Observer instance can also be accessed in the Array interceptor.

7 collection dependency

After saving the Dep instance on the attribute of Observer, we can access and collect dependencies in getter as follows:

function defineReactive(data, key, val) {
  let childOb = observe(val); // modify
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend();
      //   newly added
      if (childOb) {
        childOb.dep.depend();
      }
      return val;
    },
    set: function () {
      if (val === newVal) {
        return;
      }
      dep.notify();
      val = newVal;
    },
  });
}
/**
 * Try to create an Observer instance for value,
 * If the creation is successful, the newly created Observer instance is returned directly,
 * If value already has an Observer instance, it will be returned directly
 */
export function observe(value, asRootData) {
  //   observe is called in the defineReactive function, which passes val as a parameter and gets a return value, that is, the Observer instance
  if (!isObject(value)) {
    return;
  }
  let ob;
  //   If value is already responsive data, there is no need to create an Observer instance again,
  //   Just return the created Observer instance directly, avoiding the problem of repeatedly detecting value changes
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}

The Observer instance (childOb) of the array is obtained through observe. Finally, the dependency is collected through childOb's dep execution of the depend method.

8 get the Observer instance in the interceptor

Because the Array interceptor is an encapsulation of the prototype, you can access this (the Array currently being operated on) in the interceptor.

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}
export class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    // Add a non enumerable attribute on value__ ob__, The value of this attribute is the instance of the current Observer
    def(value, "__ob__", this); //newly added
    if (Array.isArray(value)) {
      // ....
    }
    // ....
  }
}

In this way, we can use the__ ob__ Get the attribute to the Observer instance, and then you can get it__ ob__ dep on.

Of course__ ob__ The function of is not only to access the Observer instance in the interceptor, but also to identify whether the current value has been converted into responsive data by the Observer.

In other words, there will be one on all the data that has been detected__ ob__ Property to indicate that they are responsive.

When value is marked__ ob__ After that, you can pass value__ ob__ To access the Observer instance. If it is an Array interceptor, because the interceptor is a prototype method, you can directly use this__ ob__ To access the Observer instance. The specific implementation is as follows:

[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        const ob = this.__ob__; // newly added
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

9 send notification to array dependencies

Previously, we introduced how to access the Observer instance in the interceptor, so here you only need to get the dep attribute in the Observer instance to send the notification directly:

[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        const ob = this.__ob__; 
        ob.dep.notify(); // Send message to dependency
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

10 detect the changes of elements in the array

Some elements are stored in the Array, and their changes also need to be detected. In other words, all group data of responsive data should be detected, whether it is data in Object or data in Array (each item in the Array needs to be converted into responsive).

All Observer classes handle not only Object types, but also Array types.

export class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    def(value, "__ob__", this);
    // newly added
    if (Array.isArray(value)) {
      this.observeArray(value);
    } else {
      this.walk(value);
    }
    // ....
  }
  // Detect each item in the Array and execute the observe function to detect changes
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
  // ...
}

11 detect changes in new elements

If you add array elements such as push, unshift, and splice, you also need to convert the contents into responsive ones. The interceptor will judge the type of array methods, and then use obverse to detect them.

[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        const ob = this.__ob__;
        // newly added
        let inserted;
        switch (method) {
          case "push":
          case "unshift":
            inserted = args;
            break;
          case "splice":
            inserted = args.slice(2);
            break;
        }
        if (inserted) ob.observeArray(inserted); //newly added
        ob.dep.notify();
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

12 questions about Array

this.list[0] = 2 or this.list Length = 0 cannot be monitored, and re render or watch will not be triggered.

13 summary

Array tracking changes are tracked by creating interceptors to cover the array prototype.

In order not to pollute the global array Prototype is only used for arrays that need to detect changes in Observer__ proto__ To cover the prototype method.

Array and Object collect dependencies in the same way. They are both collected in getter s. However, due to the different use of dependency locations, the array sends messages to the dependency in the interceptor, so the dependency cannot be saved in defineReactive like Object, but on the Observer instance.

In the Observer, every data detected changes is marked with__ ob__, And save this (Observer instance) in__ ob__ Up. There are two main functions, one is to mark whether the data has been detected changes, and the other is to facilitate access through the data__ ob__, So as to get the dependencies saved on the Observer instance.

In addition to detecting the changes of the array itself, the changes of the elements in the array should also be detected. Judge in the Observer that if the currently detected data is an array, call the observeArray method to convert each item in the array into a responsive one and detect the change.

In addition to detecting existing data, when users use push and other methods to add new data, the new data should also be detected for changes. In the interceptor method, judge whether it is push, unshift or splice methods, extract the new data from the parameters, and then use observeArray to detect the changes of the new data.

Note: This article is from the notes after reading vue.js (people's post and Telecommunications Press)

Tags: Javascript Front-end programming language

Posted by Tonata on Mon, 01 Aug 2022 22:54:41 +0530