JS的平凡之路–模仿Vue寫個陣列監聽

descire發表於2019-02-26

接著上一篇,本篇內容將介紹如何監聽陣列

一、分析

  相比較物件,陣列就有點複雜了,為什麼要這樣說?對於物件我們多是通過key操作的,但是陣列的操作就多了:

  • 通過length對陣列的修改;
  • 通過Index訪問和修改;
  • 通過Array.prototype上的方法修改。

1、length的監聽

            const b = [1,2,3];

            console.log(Object.getOwnPropertyDescriptor(b,`length`));複製程式碼

陣列length的描述器屬性的賦值
陣列length的描述器屬性的賦值

  一看到configuarable的值為false,我們就很絕望了,通過get,set方法來監聽它的路子是走不通了。(方法後面會講。)

2、index的監聽

  對於陣列的下標是可以採用defineProperty定義set和get方法,但是我們要注意一個問題:

            const arr = [];
            arr[3] = 1;複製程式碼

  對於上面這段程式碼,我們雖然只設定了index=3的資料,但是下標0~2會被自動填充為undefined,它的length也變成了4,所以這裡存在的問題很隱晦。

  當我們將一個陣列的下標通過defineProperty完成監聽的時候是沒什麼問題的,但是這時我們執行上面那樣的操作時,和之前的物件一樣要採用set方法新增新的下標,但是前面未定義的下標就失控了。但是下面這種方法就可以規避這種風險:

            const arr = [];
            arr.splice(3,1,10);複製程式碼

陣列的splice方法並不會產生尷尬的undefined
陣列的splice方法並不會產生尷尬的undefined

  這也就是為什麼在Vue中要通過下標訪問修改陣列時要採用set方法,而Vue的set方法的內部正是採用了splice解決這種問題。

3、陣列原型方法的監聽

  首先我們可以通過繼承Array物件,重寫原型方法實現我們的需求,那麼我們先來實現一個繼承重寫的例子:

            function Shape() {
                this.x = 0;
                this.y = 0;
            }
            Shape.prototype.move = function (x,y) {
                this.x += x;
                this.y += y;
                console.log(`shape move`);
            }
            function Rectangle() {
                Shape.call(this);
            }
            Rectangle.prototype = Object.create(Shape.prototype);
            Rectangle.prototype.constructor = Rectangle;
            Rectangle.prototype.move = function (x,y) {
                console.log(`監聽父類的move方法`);
                Shape.prototype.move.call(this,x,y);
            }
            const rect = new Rectangle();
            rect.move(20,10);複製程式碼

  這裡我們通過子類Rectangle重寫父類Shape的move方法,達到監聽的效果。是不是我們將這裡的Shape換成Array不就行了嗎?上程式碼:這裡暫且只監聽push、pop和splice,其他的基本一樣。

            function OBArray() {
                Array.apply(this, arguments);
            }
            OBArray.prototype = Object.create(Array.prototype);
            OBArray.prototype.constructor = OBArray;
            OBArray.prototype.push = function () {
                const len = Array.prototype.push.apply(this,arguments);
                console.log(`push: ` + arguments[0] + ` 新的陣列長度為:` + len);
                return len;
            }
            OBArray.prototype.pop = function () {
                const temp = Array.prototype.pop.call(this);
                console.log(`pop: ` + temp);
                return temp;
            }
            OBArray.prototype.splice = function () {
                console.log(`採用了splice方法:`);
                return Array.prototype.splice.apply(this,arguments);
            }

            const arr = new OBArray(1,2,3,4,5);複製程式碼

  這時問題就來了,你會發現只能的arr是一個空物件。但是Array原型鏈上的方法是繼承了。而這裡主要的原因就在於:

            Array.apply(this, arguments);複製程式碼

  ES6之前,JS內建物件的建構函式多不會去處理這裡的this。所以我們這裡雖然執行了Array的建構函式,但是並沒有繫結到我們此刻的this上。這裡Vue就採用了一個相當好的方法,__proto__屬性。關於__proto__和prototype的區別,所以這裡我們可以這樣改寫:

            function OBArray() {
                const arr = Array.apply(null,arguments);
                arr.__proto__ = OBArray.prototype;
                arr.constructor = OBArray;
                return arr;
            }複製程式碼

  到這裡基本就是實現陣列監聽的需求了,雖然原始碼也是這樣的思想,不過人家程式碼寫的優雅啊Vue陣列監聽的程式碼部分,很值得學習呀。

  人生不能留下遺憾,編程式碼也一樣。所以我們來看ES6給我們帶來的便利:

1、class-extends

  這裡我們可以拜讀一下阮老師的Class的繼承。文中清楚的解釋到ES5和ES6繼承的差別,下面看一下實現的程式碼:

            class OBArray extends Array {
                constructor (...args) {
                    super(...args);
                }
                push (...args) {
                    const len = super.push(...args);
                    console.log(`push: ` + args + ` 新的陣列長度為:` + len);
                    return len;
                }
            }
            const a = new OBArray(...[1,2]);
            a.push(20); // "push: 20 新的陣列長度為:3"複製程式碼

  是不是非常的方便。

2、Proxy

  對於Proxy入門,還是推薦阮老師的教程吧Proxy,程式碼如下:

            let a = new Proxy([], {
                get: function (target, key, receiver) {
                    console.log("讀取陣列");
                    return Reflect.get(target, key, receiver);
                },
                set: function (target, key, receiver) {
                    console.log(`設定陣列`);
                    return Reflect.set(target, key, receiver);
                }
            })

            a.splice(3,1,10);複製程式碼

  這裡控制檯輸出很有趣,有興趣的可以研究研究。也正是通過Proxy我們可以實現對length的監聽,不過相容性是硬傷啊。。。。

二、總結

  到這裡基本上陣列的監聽的學習就完成啦。無論你用古老的”__proto__”,還是用es6的extends和Proxy實現,我們最主要的還是要理解Array這個物件的特點,哎,基礎並不是我們想象的那麼基礎呀!

參考資料
知乎proto和prototype
阮老師的Class-extends
阮老師的Proxy
Vue core array.js原始碼

您的關注是我學習的最大動力^_^

相關文章