JS的平凡之路--模仿Vue寫個簡單的物件監聽

descire發表於2017-06-28

本篇實現的僅僅是物件監聽,不包含陣列。關於陣列的下一篇會講。

一、用什麼來監聽物件?

  Vue給了我們很好的回答,那就是defineProperty。defineProperty方法有以下幾個引數:

  • obj: 你要設定屬性的物件;
  • propName: 你設定的屬性的名稱;
  • descriptor: 描述符物件;

  這個descriptor物件可以設定的屬性就比較多了:

  • value: 設定屬性的值;
  • writable: 預設為false,如果該值為false,則不能再修改屬性的值;
  • enumerable: 預設為false,如果該值為false,則不能被列舉;
  • configurable: 預設為false,如果該值為false,那麼descriptor不能再被修改,也不能使用delete;
  • get與set:存取器方法,預設為undefined。

  其他的descriptor物件理解應該不難,看了MDN上的教程,應該會發現對於set和get方法的使用,總是會在外部宣告一個變數來維護值的變化,是不是很變扭,然後就會有人嘗試用this來呼叫屬性,這時就會報執行棧溢位,因為它遞迴的執行自己,導致棧溢位。

  這裡我們可以通過set和get內部維護的'_'開頭的私有變數來解決:

            const obj = {};
            Object.defineProperty(obj,'name', {
                get () {
                    console.log('讀取name的值為 => ' + this._name);
                    return this._name;
                },
                set (newValue) {
                    this._name = newValue;
                    console.log('設定name的值為 => ' + this._name);
                }
            })
            obj.name = 'xiaoming'; // '設定name的值為 => xiaoming'
            obj.name; // '讀取name的值為 => xiaoming'複製程式碼

  兩種方法看著喜好用吧。

二、實現物件的監聽

  首先我們得有一個物件:

            const obj = {
                a: 1,
                b: {
                    c: 2,
                    d: 3
                }
            };複製程式碼

  接下來我們要將它變成一個響應式物件,讓它的取值與賦值操作多在我們的監聽下,首先我們要通過一個Observer來包裝我們需要響應化的物件:

            function Observer(value) {
                /**
                 * 建構函式防止沒有使用'new'關鍵字建立
                 */
                if (!(this instanceof Observer)) {
                    return new Observer(value);
                }
                this.value = value;
                // 這一篇僅僅討論物件的監聽
                if (!Array.isArray(value)) {
                    this.walk(value);
                }
            }

            Observer.prototype.walk = function (obj) {
                const keys = Object.keys(obj);
                for (let i = 0, max = keys.length; i < max; i++) {
                    const key = keys[i];
                    //將物件響應化
                    defineReactive(obj, key, obj[key]);
                }
            }複製程式碼

  接下來核心就是採用defineProperty方法來監聽set和get方法,但是這裡我們要注意的是採用遞迴建立Observer物件的方式,解決物件巢狀的問題:

            function defineReactive(obj, key, val) {
                //獲取物件的描述器物件
                const property = Object.getOwnPropertyDescriptor(obj, key);
                //這裡上面強調了configurable屬性的重要性
                if (property && property.configurable === false) {
                    return;
                }

                const setter = property && property.set;
                const getter = property && property.get;

                //解決物件巢狀的問題
                observe(val);

                Object.defineProperty(obj, key, {
                    enumerable: true,
                    configurable: true,
                    get () {
                        const value = getter ?  getter.call(obj) : val;
                        log('讀取',obj,key,value);
                        return value;
                    },
                    set (newValue) {
                        const value = getter ? getter.call(obj) : val;
                        if (setter) {
                            setter.call(obj, newValue);
                        } else {
                            val = newValue;
                        }
                        log('設定',obj,key,value);
                    }
                })
            }

            function observe(obj) {
                if (!isObject(obj)) {
                    return;
                }
                return new Observer(obj);
            }複製程式碼

  這裡基本上物件的監聽已經完成了,下面來試試:

        Observer(obj);
        obj.b.d = 10;複製程式碼

控制檯輸出結果
控制檯輸出結果

  但是這裡還存在一個問題,因為我們還有這種操作:

        obj.f = 10;複製程式碼

  這時obj.f不再被我們監聽,對於這種情況在Vue中我們要通過Vue.set方法設定,這裡我們也可以寫了set方法:

            function set(target, key, val) {
                if(!target.hasOwnProperty(key)) {
                    target[key] = val;
                    defineReactive(target, key, val);
                }
            }複製程式碼

  這裡是個簡易的set方法,主要思想就是使新屬性響應化。

三、總結

  通過原始碼的學習,使你在使用Vue的一些api更加得心應手。

  PS:原始碼用的flow.js,看起來有點費勁,但是我應該會堅持下去。(下一篇會講陣列的監聽)

參考資料
Vue core部分observer
熊建剛部落格

相關文章