從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繼而能夠創造對應類型的實例。

  上面整個的文章都是基於監聽數組響應的一個點想到的。這裏僅僅是起到拋磚引玉的做用,但願能對你們有所幫助。若有不正確的地方,歡迎你們指出,願共同窗習。

相關文章
相關標籤/搜索