摘要:因為無法通過借用構造函數的方式創建響應式屬性雖然屬性可以被創建,但不具備響應式功能,因此在我們是沒法繼承數組的。上面整個的文章都是基于監聽數組響應的一個點想到的。
前言
首先歡迎大家關注我的Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。
從上一篇文章響應式數據與數據依賴基本原理開始,我就萌發了想要研究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);直接return出來呢?
如果我們return這個返回的數組,這個數組是由原生的Array構造出來的,所以它的push等方法依然是原生數組的方法,無法到達重寫的目的。
首先我們知道采用new操作符調用構造函數會依次經歷以下四個步驟:
創建新對象
將構造函數的作用域給對象(因此構造函數中的this指向這個新對象)
執行構造函數的代碼
返回新對象(如果沒有顯式返回的情況下)
在沒有顯式返回的時候,返回的是新對象,因此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,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的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.speciesSymbol.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繼而可以創造對應類型的實例。
上面整個的文章都是基于監聽數組響應的一個點想到的。這里僅僅是起到拋磚引玉的作用,希望能對大家有所幫助。如有不正確的地方,歡迎大家指出,愿共同學習。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95443.html
摘要:響應式數據是在模塊中實現的我們可以看看是如何實現的。早期代碼使用是進行單元測試,是事件模型的單元測試文件。模塊實際上采用采用組合繼承借用構造函數原型繼承方式繼承了其目的就是繼承的,等方法。 前言 首先歡迎大家關注我的Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。接下來的日子我應該會著力寫一系列關于Vue與React內部原...
摘要:但是當傳遞的參數為對象或者數組的時候,是通過引用傳入的,所以對于一個引用類型的來說,在子組件中改變這個參數本身將會影響到父組件的數據狀態。 問題 父級組件與子組件的通信一般都是通過props來實現的,因為數據流向的單一才能保證數據變化的可追蹤性,在vue中props遵循的是單向數據流,原則上子組件修改props是不被允許的。但是當props傳遞的參數為對象或者數組的時候,是通過引用傳入...
摘要:直到有一天你會碰到線上奇奇怪怪的問題,如線程執行一個任務遲遲沒有返回,應用假死。正好這次借助之前的一次生產問題來聊聊如何排查和解決問題。本地模擬上文介紹的是線程相關問題,現在來分析下內存的問題。盡可能的減少多線程競爭鎖。 showImg(https://segmentfault.com/img/remote/1460000015568421?w=2048&h=1150); 前言 之前或...
摘要:這里加了個簡單判斷,只看數組元素的,然后寫了一個簡單案例,主要測試使用改變數組元素能不能被監測到,并響應式的渲染頁面運行頁面可以看到,運行了次,我們數組長度為,也就是說數組被遍歷了兩遍。 問題來源:https://segmentfault.com/q/10... 問題描述:Vue檢測數據的變動是通過Object.defineProperty實現的,所以無法監聽數組的添加操作是可以理解的...
摘要:也給當初出入迷宮的我不小考驗,一道題目可以引發許多思考,今天寫下的只是今時今日的想法,到未來也許還有別樣的看法。對于回調函數,可以對其傳入三個參數分別是當前元素,元素索引,調用的數組。 [1,2,3].map(parseInt) 這道JS題目,相信大家并不會陌生。也給當初出入JS迷宮的我不小考驗,一道題目可以引發許多思考,今天寫下的只是今時今日的想法,到未來也許還有別樣的看法。...
閱讀 2655·2021-11-23 09:51
閱讀 1644·2021-11-22 13:54
閱讀 2782·2021-11-18 10:02
閱讀 936·2021-08-16 10:57
閱讀 3554·2021-08-03 14:03
閱讀 1873·2019-08-30 15:54
閱讀 3527·2019-08-23 14:39
閱讀 598·2019-08-23 14:26