Vue Listening Array Change Source Code Analysis

  • 2021-08-03 08:34:50
  • OfStack

In the code of the first article, ignoring the processing of the array, only concerned about the part that needs to be concerned, pretending that the array does not exist.

This article begins to consider the problem of arrays.

Start with the simplest

First consider a question, how to listen for changes in objects in an array. Ignore the array itself and its 1-value, and only consider the objects in the object array.

Traverse the array, and then call the observe method on each object in the array


//  Upper 1 Unrewritten code that appears in the article, this 1 No repetition in the article 
var Observer = function Observer(value) {
  this.value = value;
  this.dep = new Dep();
  //  If it is an array, all elements are traversed 
  if(Array.isArray(value)) {
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};
Observer.prototype.observeArray = function observeArray(items) {
  //  Traverse all elements of the array, and perform a single element  getter , setter  Binding 
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

Realistic requirements

Of course, the actual implementation will not be as simple as the above example. The official document describes the listening array as follows:

Vue contains a set of mutation methods for viewing arrays, so they will also trigger view updates. These methods are as follows:
push (), pop (), shift (), unshift (), splice (), sort (), reverse ()

Due to the limitations of JavaScript, Vue cannot detect the following changing arrays:

When you set the index of 1 item directly, for example: vm. items [indexOfItem] = newValue
When you modify the length of the array, for example: vm. items. length = newLength
Therefore, we should listen to 1 methods of the array itself.

A small function that is often used

def, recurring throughout the Vue source code, uses Object. defineProperty () to define the attribute key on obj (or possibly modify the existing attribute key):


function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    //  Turn into  boole  Value, if you don't pass parameters, change to  false
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

Add 1 set of methods to an object

Add a set of methods to the object. If the environment supports proto, it is simple. It is good to directly point the proto of the object to this set of methods; If not, iterate through the 1 set of methods and add them to the object in turn as hidden attributes (i.e. enumerable: false, not found by the in keyword):


var hasProto = '__proto__' in {};
var augment = hasProto ? protoAugment : copyAugment;

function protoAugment(target, src) {
  target.__proto__ = src;
}
function copyAugment(target, src, keys) {
  for(var i = 0; i < keys.length; i++) {
    var key = keys[i];
    def(target, key, src[key]);
  }
}

Let's start with a simple one


var arrayPush = {};

(function(method){
  var original = Array.prototype[method];
  arrayPush[method] = function() {
    // this  Pointing can be seen by the following test 
    console.log(this);
    return original.apply(this, arguments)
  };
})('push');

var testPush = [];
testPush.__proto__ = arrayPush;
//  From the output, you can see that the above  this  It points to  testPush
// []
testPush.push(1);
// [1]
testPush.push(2);

Pseudo-overwrite array prototype to listen for array changes

As stated in the official document, only seven methods need to be monitored: push (), pop (), shift (), unshift (), splice (), sort () and reverse (). These seven methods can be divided into two categories:

1. push (), unshift () and splice (), which may add new elements to the array;

2. The rest of the methods that do not add elements.

In order to avoid polluting the global Array, a new object based on Array. prototype is created, and then attributes are added to the object itself, and then the newly created object is added to value of Observer as a prototype or attribute to monitor its changes.


var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

The next step is to iterate over the methods that need to trigger updates, attaching them in turn to the arrayMethods:


['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
  //  Gets the original array manipulation method 
  var original = arrayProto[method];
  //  In  arrayMethods  Create a new property on  method And for  method  Specify a value (function) 
  //  That is, rewriting  arrayMethods  Array method with the same name on 
  def(arrayMethods, method, function mutator() {
    var arguments$1 = arguments;

    var i = arguments.length;
    var args = new Array(i);
    //  Pseudo-array  arguments  Convert to array form 
    //  Why not  [].slice.call(arguments) ? 
    while(i--) {
      args[i] = arguments$1[i];
    }
    var result = original.apply(this, args);
    //  Cause  arrayMethods  Is to act as  Observer  In  value  The prototype of or directly as an attribute, so the  this 1 Like is pointing  Observer  In  value
    //  Of course, it needs to be modified  Observer So that the  value  Have 1 Point  Observer  Its own attributes, __ob__ To link the two 
    var ob = this.__ob__;
    //  Store new array elements 
    var inserted;
    //  Consider several methods that may have new elements separately 
    switch(method) {
      case 'push':
        inserted = args;
        break;
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        // splice  Methodology No. 1 3 A new element is added at the beginning of a parameter 
        inserted =args.slice(2);
        break;
    }
    if(inserted) {
      //  For the newly added element  getter , setter  Binding 
      ob.observerArray(inserted);
    }
    //  Trigger method 
    ob.dep.notify();
    return result;
  });
};

var arrayKeys = Object.getOwnPropertyNames(arrayMethods);

Update Observer

According to the comments in the above example code, rewrite Observer, so that the two are related to achieve the purpose of listening for array changes:


var Observer = function Observer(value) {
  this.value = value;
  this.dep = new Dep();
  def(value, '__ob__', this);
  //  If it is an array, all elements are traversed 
  if(Array.isArray(value)) {
    var argument = hasProto ? protoAugment : copyAugment;
    argument(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

References:
vue Early Source Learning Series 2: How to Listen for Changes in an Array


Related articles: