Размышления об отзывчивости массивов Vue

внешний интерфейс GitHub JavaScript Vue.js
Размышления об отзывчивости массивов Vue

предисловие

   Прежде всего, приглашаю всех подписаться на меняБлог на гитхабе, что можно рассматривать как небольшое поощрение для меня, ведь у меня нет денег, чтобы писать вещи, и я могу продолжать на своем собственном энтузиазме и всеобщем поощрении.

   Из предыдущего постаРеактивные данные и основы зависимости от данныхВ начале у меня была идея изучить исходный код Vue. Я недавно прочитал статью YoungwindКак прослушивать изменения в массивеЯ обнаружил, что ранняя реализация прослушивающих массивов Vue немного отличалась от моей реализации. И автор неправильно понял часть кода два года назад, прочитав комментарии @Ma63dКомментарийПосле этого я почувствовал много преимуществ.

Как Vue реализует мониторинг данных

   В нашей прошлой статье мы хотели попробовать отслеживать изменения массива, используя следующую идею:

function observifyArray(array){
  //需要变异的函数名列表
  var methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
  var arrayProto = Object.create(Array.prototype);
  _.each(methods, function(method){
    arrayProto[method] = function(...args){
      // 劫持修改数据
      var ret = Array.prototype[method].apply(this, args);
      //可以在修改数据时触发其他的操作
      console.log("newValue: ", this);
      return ret;
    }
  });
  Object.setPrototypeOf(array, arrayProto);
}

   Мы делаем это, устанавливая прототип экземпляра массиваprototypeдостичь, новыйprototype重写了原生数组原型的部分方法。因此在调用上面的几个变异方法的时候我们会得到相应的通知。 Но по фактуsetPrototypeOfЭто метод ECMAScript 6, который, безусловно, не является дополнительной реализацией VUE. Мы можем в общих чертах взглянуть на реализацию VUEидеи.

function observifyArray(array){
    var aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
    var arrayAugmentations = Object.create(Array.prototype);
    
    aryMethods.forEach((method)=> {
    
        // 这里是原生Array的原型方法
        let original = Array.prototype[method];
       // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
       // 注意:是属性而非原型属性
        arrayAugmentations[method] = function () {
            console.log('我被改变啦!');
            // 调用对应的原生方法并返回结果
            return original.apply(this, arguments);
        };
    });
    array.__proto__ = arrayAugmentations;
}

  __proto__Всем нам это очень знакомое свойство, которое указывает на объект-прототип, соответствующий объекту-экземпляру. В ES5 в каждом экземпляре есть внутреннее свойство[[Prototype]]Указывает на объект-прототип, соответствующий объекту-экземпляру, но доступ к внутренним свойствам недоступен. Производители браузеров поддерживают нестандартные атрибуты__proto__. На самом деле идея реализации Vue очень похожа на нашу. Единственное отличие — нестандартные свойства, которые использует Vue.__proto__.

   Фактически, учащиеся, прочитавшие «Продвинутое программирование на JavaScript», должны помнить о прототипном наследовании. Важной идеей является создание объектов на основе существующих объектов с помощью прототипов. Например:

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

   На самом деле, наша идея Vue выше такая же.Мы создаем новый экземпляр на основе arrayAugmentations с помощью прототипа, чтобы экземпляр мог получить доступ к нашему пользовательскому методу мутации.

   Автор вышеуказанной статьи, youngwind, при написании статьи предложил, почему бы не использовать для реализации более распространенное композиционное наследование, например:

function FakeArray() {
    Array.apply(this,arguments);
}

FakeArray.prototype = [];
FakeArray.prototype.constructor = FakeArray;

FakeArray.prototype.push = function () {
    console.log('我被改变啦');
    return Array.prototype.push.apply(this,arguments);
};

let list = ['a','b','c'];

let fakeList = new FakeArray(list);

оказатьсяfakeListНе массив, а объект, подумал тогда автор:

По умолчанию конструктор возвращает объект this, который является объектом, а не массивом. Array.apply(this,arguments); Этот оператор возвращает массив

Можем ли мы вернуть Array.apply(this, arguments); напрямую?

Если мы вернем возвращенный массив, этот массив будет создан из нативного массива, поэтому его push и другие методы по-прежнему будут нативными методами массива, которые не могут достичь цели перезаписи.

Сначала мы знаем, чтоnewОператор последовательно вызывает конструктор, выполняя следующие четыре шага:

  1. создать новый объект
  2. Дает область конструктора объекту (так что это в конструкторе указывает на этот новый объект)
  3. код, который выполняет конструктор
  4. Возвращает новый объект (если не возвращен явно)

   При отсутствии явного возврата возвращается новый объект, поэтомуfakeListявляется объектом, а не массивом. Но почему нельзя заставить вернутьсяArray.apply(this,arguments). На самом деле, кто-то ниже сказал, что у автора проблема с этим предложением

Этот массив создан из собственного массива, поэтому его push и другие методы по-прежнему являются собственными методами массива, которые не могут достичь цели перезаписи.

   На самом деле, приведенное выше предложение само по себе не является неправильным.Когда мы явно возвращаемся к конструктору, мы получаемfakeListэто исходный массив. так позвониpushметод незаметен. Но мы не можем вернутьсяArray.apply(this,arguments)Более глубокая причина в том, что мы вызываем здесьArray.apply(this,arguments)Цель состоит в том, чтобы заимствовать роднойArrayКонструктор будетArrayСвойство присваивается текущему объекту.

Например:

function Father(){
 this.name = "Father";
}

Father.prototype.sayName = function(){
 console.log("name: ", this.name);
}

function Son(){
 Father.apply(this);
 this.age = 100;
}

Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
 console.log("age: ", this.age);
}


var instance = new Son();
instance.sayName(); //name:  Father
instance.sayAge(); //age:  100

ПодклассSonЧтобы наследовать от родительского классаFatherСвойства и методы вызываются дваждыFatherконструктор,Father.apply(this)Это создание свойств родительского класса иSon.prototype = new Father();Цель состоит в том, чтобы наследовать методы родительского класса через цепочку прототипов. Итак, выше сказано, почему нельзяArray.apply(this,arguments)Причина вынужденного возврата, его цель - одолжить родноеArrayКонструктор создает соответствующие свойства.

   Но вопрос, а почему нельзя взять роднойArrayКак насчет конструкторов, которые создают объекты? На самом деле не толькоArray,String,Number,Regexp,ObjectИ т. д. Встроенные классы JavaScript не могут создавать свойства с функциями, заимствуя конструкторы (например:length). В массивах JavaScript есть особое реактивное свойство.length, с одной стороны, если изменятся данные нижнего индекса числового типа массива, то он будет вlengthВышеприведенный вариант осуществления, с другой стороны, изменяетlengthТакже влияет на числовые данные массива. Потому что невозможно создать реактивный, позаимствовав конструкторlengthСвойства (хотя свойства можно создавать, они не реактивны), поэтому в E55 мы не можем наследовать массивы. Например:

function MyArray(){
    Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
});

var colors = new MyArray();
colors[0] = "red"; 
console.log(colors.length); // 0

colors.length = 0;
console.log(colors[0]); //"red"

  К счастью, мы встречаем рассвет ES6.Благодаря расширениям класса class мы можем реализовать наследование нативных массивов, например:

class MyArray extends Array {
}

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0

colors.length = 0;
cosole.log(colors[0]); // undefined

  Почему расширения ES6 могут обеспечить наследование массивов, чего не может ES5? Это связано с разными принципами наследования двух. В методе наследования ES5 сначала создается производный тип.this(например, MyArray), а затем вызвать конструктор базового класса (например, Array.apply(this)), что означаетthisСначала указывается экземпляр производного класса, а затем — экземпляр базового класса. Поскольку собственные объекты (например, массив) не могут быть предоставлены путем заимствованияthisназначатьlengthПодобные свойства являются функциональными, поэтому мы не можем добиться желаемого результата.

   Но ES6extendsМетод наследования противоположен, сначала создается базовым классом (массивом).this, который затем модифицируется конструктором производного класса, поэтому в приведенном выше примере можно передатьthisСоздает все встроенные функции базового класса и принимает связанные с ним функции (такие какlength), то здесьthisНа основе расширения с помощью производных классов мы можем достичь нашей цели наследования собственных массивов.

Больше чем это. ES6 также предоставляет очень удобное свойство при расширении нативных объектов, как показано выше:Symbol.species.

Symbol.species

  Symbol.speciesОсновная функция состоит в том, чтобы сделатьПервоначально возвращает экземпляр базового классаМетод наследования возвращает пример партии, например, такой какArray.prototype.sliceТо, что возвращается, является экземпляром массива, но когдаMyArrayнаследоватьArray, мы также надеемся, что при использованииMyArrayэкземпляр вызоваsliceтакже может вернутьсяMyArrayпример. Итак, как мы его используем?Symbol.speciesявляется статическим свойством доступа, если оно определено при определении производного класса, оно может достичь нашей цели. Например:

class MyArray extends Array {
  static get [Symbol.species](){
    return this;
  }
}

var myArray = new MyArray(); // MyArray[]
myArray.slice(); // MyArray []

   Мы можем найти случаи вызова подклассов массиваmyArrayизsliceметод также вернетMyArrayэкземпляр типа. Если вам нравится экспериментировать, вы обнаружите, что даже без статического свойства доступаget [Symbol.species],myArray.slice()все равно вернетсяMyArrayЭкземпляр, это потому, что даже если вы не определите его явно, по умолчаниюSymbol.speciesсвойства также вернутсяthis. Конечно, вы такжеthisИзмените на другое значение, чтобы изменить тип экземпляра, возвращаемый соответствующим методом. Например, я хочу экземплярmyArrayизsliceМетод возвращает собственный тип массиваArray, можно использовать следующее определение:

class MyArray extends Array {
  static get [Symbol.species](){
    return Array;
  }
}

var myArray = new MyArray(); // []
myArray.slice(); // []

   Конечно, если в приведенном выше примере, если вы хотите, чтобы тип экземпляра, возвращаемый пользовательской функцией, был таким же, какSymbol.speciesЕсли тип непротиворечив, его можно определить следующим образом:

class MyArray extends Array {
  static get [Symbol.species](){
    return Array;
  }
  
  constructor(value){
    super();
    this.value = value;
  }
  
  clone(){
    return new this.constructor[Symbol.species](this.value)
  }
}

var myArray = new MyArray();
myArray.clone(); //[]

   В приведенном выше коде мы можем видеть это в методе экземпляра, вызвавthis.constructor[Symbol.species]мы можем получитьSymbol.speciesЗатем может быть создан экземпляр соответствующего типа.

   Вся статья выше основана на прослушивании ответов массива. Это просто играет роль привлечения нефрита, и я надеюсь, что это может быть полезно для всех. Если что-то не так, пожалуйста, укажите на это, и мы готовы учиться вместе.