从Vue数组响应化所引起的思考

前言

  首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励。javascript

  从上一篇文章响应式数据与数据依赖基本原理开始,我就萌发了想要研究Vue源码的想法。最近看了youngwind的一篇文章如何监听一个数组的变化发现Vue早期实现监听数组的方式和个人实现稍有区别。而且在两年前做者对其中的一些代码的理解有误,在阅读完评论中@Ma63d的评论以后,感受收益匪浅。java

Vue实现数据监听的方式

  在咱们的上一篇文章中,咱们想尝试监听数组变化,采用的是下面的思路:git

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的实现思路github

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的新实例,使得实例可以访问到咱们自定义的变异方法。app

  上面一篇文章的做者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);这个语句返回的才是数组ui

咱们能不能将Array.apply(this,arguments);直接return出来呢?

若是咱们return这个返回的数组,这个数组是由原生的Array构造出来的,因此它的push等方法依然是原生数组的方法,没法到达重写的目的。

首先咱们知道采用new操做符调用构造函数会依次经历如下四个步骤:

  1. 建立新对象
  2. 将构造函数的做用域给对象(所以构造函数中的this指向这个新对象)
  3. 执行构造函数的代码
  4. 返回新对象(若是没有显式返回的状况下)

  在没有显式返回的时候,返回的是新对象,所以fakeList是对象而不是数组。可是为何不能强制返回Array.apply(this,arguments)。其实下面有人说做者这句话有问题

这个数组是由原生的Array构造出来的,因此它的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,StringNumberRegexpObject等等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的extends,咱们就能够实现继承原生的数组,例如:

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的extends能够作到ES5所不能实现的数组继承呢?这是因为两者的继承原理不一样致使的。ES5的继承方式中,先是生成派生类型的this(例如:MyArray),而后调用基类的构造函数(例如:Array.apply(this)),这也就是说this首先指向的是派生类的实例,而后指向的是基类的实例。因为原生对象(例如: Array)经过借用的方式并不能给this赋值length相似的具备功能的属性,所以咱们无法实现想要的结果。

  可是ES6的extends的继承方式倒是与之相反的,首先是由基类(Array)建立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 []
复制代码

  咱们能够发现调用数组子类的实例myArrayslice方法时也会返回的是MyArray类型的实例。若是你喜欢尝试的话,你会发现即便去掉了静态访问器属性get [Symbol.species]myArray.slice()也会仍然返回MyArray的实例,这是由于即便你不显式定义,默认的Symbol.species属性也会返回this。固然你也将this改变为其余值来改变对应方法的返回的实例类型。例如我但愿实例myArrayslice方法返回的是原生数组类型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继而能够创造对应类型的实例。

  上面整个的文章都是基于监听数组响应的一个点想到的。这里仅仅是起到抛砖引玉的做用,但愿能对你们有所帮助。若有不正确的地方,欢迎你们指出,愿共同窗习。

相关文章
相关标签/搜索