This is the third day of my participation in Gwen Challenge.

preface

Previously we looked at detecting changes in objects, so why is Array a separate topic? Let’s use the following example to illustrate:

this.list.push(1)
Copy the code

Object can detect state through getter/setter, but array push method cannot trigger getter/setter. In this article, we’ll learn how Array implements change detection.

How to track change

Object notifies dependencies on Update via setters. If we can notify the array push, we can achieve the same effect.

We can point the Array prototype to a new object that overrides the Array method on array. prototype.

The interceptor

The name for the object that has both array methods and notification capabilities is an interceptor (which is also an embodiment of the proxy pattern).

How do you implement an interceptor?

Array.prototype has seven methods that can change the contents of an Array: push, POP, Shift, unshift, spice, and reverse.

const arrayProto = array.prototype;

export const arrayMethods = Object.create(arrayProto);

[
    'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function (method){
    // Cache the method on the prototype
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        enumerable: false.writable: true.configurable: true.value: function mutator(. args){
            // ...
            // Invoke the method on the prototype
            return original.apply(this.args)
        }
    })
})
Copy the code

Now that we have interceptors, how do we make them work? The violent way is to modify array. prototype directly, but that would pollute the global Array, so we’ll try a different approach — pointing the Array instance’s prototype at the interceptor.

  1. using__proto__
  2. Using the ES6Object.setPrototypeOf()

For compatibility reasons, we use the first method, which copies the interceptor’s methods directly onto the array instance if __proto__ is not supported.

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

function copyAugment(target, src, keys) {
  for (let i = 0, len = keys.length; i < len; i++) {
    constkey = keys[i]; def(target, key, src[key]); }}function def(target, key, val, enumerable? : boolean) {
  Object.defineProperty(target, key, {
    enumerable:!!!!! enumerable,writable: true.configurable: true.value: val,
  });
}
Copy the code

Let’s now remake the Observer:

const hasProto = '__proto__' in {};
export default class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value); // Convert all properties of the object to getters/setters}}walk(obj) {
    Object.keys(obj).forEach((key) = >{ defineReactive(obj, key, obj[key]); }); }}Copy the code

How to collect dependencies

We implemented the interceptor above, but the interceptor does not yet have notification dependencies. To implement notification dependencies, you must first implement the dependency collection function. So how does an array collect dependencies?

Let’s start by reviewing how Object dependencies are collected. An Object’s dependency collection is collected using a Dep instance in the getter, and each key has a Dep to collect dependencies.

In fact, arrays also collect dependencies in the getter.

{
    list: [1.2.3.4]}Copy the code

When a list is read, the getter for the list property fires.

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();
      // Collect array dependencies
      return val;
    },
    set: function (newVal) {
      if (val === newVal) return; dep.notify(); val = newVal; }}); }Copy the code

Array collects dependencies in the getter and triggers dependencies in the interceptor.

Where is the dependency collection

Vue.js stores Array dependencies in an Observer instance:

export default class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep(); // Array dependencies are collected here
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value); // Convert all properties of the object to getters/setters}}}Copy the code

Collect rely on

After adding a DEP attribute to the Observer instance, we can collect dependencies.

function defineReactive(data, key, val) {
  let childOb = observe(val);
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true.configurable: true.get: function () {
      dep.depend();
      / / new
      if (childOb) {
        childOb.dep.depend();
      }
      return val;
    },
    set: function (newVal) {
      if (val === newVal) return; dep.notify(); val = newVal; }}); }function observe(value, asRootData) {
  if (typeofvalue ! = ="object") {
    return;
  }
  let ob;
  if (value.hasOwnProperty("__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}
Copy the code

Notification dependency update

Now that we’ve done the dependency collection for arrays, we just need to notify dependencies. To notify dependencies, we need to have access to the DEP on the Observer instance, so how do we access the DEP in the interceptor?

Careful students may have noticed that __ob__ appears in the observe function above, and that’s the core.

export default class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    def(value, '__ob__'.this); / / new
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value); // Convert all properties of the object to getters/setters}}}Copy the code

We define a new attribute __ob__ on value to point to the Observer instance, and then we can call the Observer instance on value in the interceptor, thus accessing its DEP attribute.

const arrayProto = array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
    'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function (method){
    constt original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        enumerable: false.writable: true.configurable: true.value: function mutator(. args){
            let ob = value.__ob__; / / new
            ob.dep.notify() / / new
            return original.apply(this.args)
        }
    })
})
Copy the code

Detects changes to elements in arrays

Above, we implemented dependency collection and dependency notification for arrays. So what if there are objects in the array?

If the properties of an object in the array change, it makes sense to send notifications as well. Also, if you add an object to the array, you need to turn that object into a responsive object. So, we need to go through the array, trying to convert the elements of the array into responsive.

export default class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      this.observeArray(value); / / new
    } else {
      this.walk(value); }}observerArray(list) {
    for (let i = 0, len = list.length; i < l; i++) {
      observe(list[i]); // Try to convert each item into a responsive one}}}Copy the code

New element in detection array

We can try to convert the new element to responsiveness by passing it to the __ob__ observeArray method in the interceptor.

const arrayProto = array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
    'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function (method){
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        enumerable: false.writable: true.configurable: true.value: function mutator(. args){
            const result = original.apply(this.args)
             let ob = value.__ob__; 
            // add the new element to try to make it responsive
            let inserted;
            switch(metthod){
                case 'push':
                case 'unshift':
                    inserted = args;
                    break;
                case 'splice':
                    inserted = args.slice(2);
                    break;
            }
            if (inserted) ob.serveArray(inserted);
            ob.dep.notify() 
            
            returnresult; }})})Copy the code

The problem of Array

The change is not detectable by modifying elements directly by subscript and emptying the array with list.length = 0.

conclusion

How is Array change detection implemented

  • Unlike Object, we point the prototype of the array instance to the interceptor we defined, collect the dependencies in the getter, and notify the dependencies in the interceptor.
  • To collect dependencies in the getter, we add an instance attribute, dep, to the Observer
  • To enable notification of dependencies, we definevalue.__ob__Property pointing to an observer instance, used in interceptorsvalue.__ob__.dep.notify()To notify dependencies.
  • Considering that the elements in the array may be objects, in order to detect the changes of the object elements in the array, we try to convert the elements in the array into responses and add them to the observerobserveArrayMethod, which can be used to convert elements of an array into responses at initialization, or called from the array’s interceptorvalue.__ob__.observeArray(inserted)And try to make the new element responsive.