從 JavaScript 屬性描述器剖析 Vue.js 響應式檢視

熊建剛發表於2017-06-09

學習每一門語言,一般都是從其資料結構開始,JavaScript也是一樣,而JavaScript的資料結構中物件(Object)是最基礎也是使用最頻繁的概念和語法,坊間有言,JavaScript中,一切皆物件,基本可以描述物件在JavaScript中的地位,而且JavaScript中物件的強大也使其地位名副其實,本篇介紹JavaScript物件屬性描述器介面及其在資料檢視繫結方向的實踐,然後對Vue.js的響應式原理進行剖析。

可以先看一個應用例項,點選此處

前言

JavaScript的物件,是一組鍵值對的集合,可以擁有任意數量的唯一鍵,鍵可以是字串(String)型別或標記(Symbol,ES6新增的基本資料型別)型別,每個鍵對應一個值,值可以是任意型別的任意值。對於物件內的屬性,JavaScript提供了一個屬性描述器介面PropertyDescriptor,大部分開發者並不需要直接使用它,但是很多框架和類庫內部實現使用了它,如avalon.js,Vue.js,本篇介紹屬性描述器及相關應用。

定義物件屬性

在介紹物件屬性描述之前,先介紹一下如何定義物件屬性。最常用的方式就是使用如下方式:


    var a = {
        name: 'jh'
    };

    // or 
    var b = {};
    b.name = 'jh';

    // or
    var c = {};
    var key = 'name';
    c[key] = 'jh';複製程式碼

本文使用字面量方式建立物件,但是JavaScript還提供其他方式,如,new Object(),Object.create(),瞭解更多請檢視物件初始化

Object.defineProperty()

上面通常使用的方式不能實現對屬性描述器的操作,我們需要使用defineProperty()方法,該方法為一個物件定義新屬性或修改一個已定義屬性,接受三個引數Object.defineProperty(obj, prop, descriptor),返回值為操作後的物件:

  • obj, 待操作物件
  • 屬性名
  • 操作屬性的屬性描述物件

    var x = {};
    Object.defineProperty(x, 'count', {});
    console.log(x); // Object {count: undefined}複製程式碼

由於傳入一個空的屬性描述物件,所以輸出物件屬性值為undefined,當使用defineProperty()方法操作屬性時,描述物件預設值為:

  • value: undefined
  • set: undefined
  • get: undefined
  • writable: false
  • enumerable: false,
  • configurable: false

不使用該方法定義屬性,則屬性預設描述為:

  • value: undefined
  • set: undefined
  • get: undefined
  • writable: true
  • enumerable: true,
  • configurable: true

預設值均可被明確引數值設定覆蓋。

當然還支援批量定義物件屬性及描述物件,使用`Object.defineProperties()方法,如:


    var x = {};
    Object.defineProperties(x, {
        count: {
            value: 0
        },
        name: {
            value: 'jh'
        }
    });
    console.log(x); // Object {count: 0, name: 'jh'}複製程式碼

讀取屬性描述物件

JavaScript支援我們讀取某物件屬性的描述物件,使用Object.getOwnPropertyDescriptor(obj, prop)方法:


    var x = {
        name: 'jh'
    };
    Object.defineProperty(x, 'count', {});
    Object.getOwnPropertyDescriptor(x, 'count');
    Object.getOwnPropertyDescriptor(x, 'name');

    // Object {value: undefined, writable: false, enumerable: false, configurable: false}
    // Object {value: "jh", writable: true, enumerable: true, configurable: true}複製程式碼

該例項也印證了上面介紹的以不同方式定義屬性時,其預設屬性描述物件是不同的。

屬性描述物件

PropertyDescriptor API提供了六大例項屬性以描述物件屬性,包括:configurable, enumerable, get, set, value, writable.

value

指定物件屬性值:


    var x = {};
    Object.defineProperty(x, 'count', {
        value: 0
    });
    console.log(x); // Object {count: 0}複製程式碼

writable

指定物件屬性是否可變:


    var x = {};
    Object.defineProperty(x, 'count', {
        value: 0
    });
    console.log(x); // Object {count: 0}
    x.count = 1; // 靜默失敗,不會報錯
    console.log(x); // Object {count: 0}複製程式碼

使用defineProperty()方法時,預設有writable: false, 需要顯示設定writable: true

存取器函式(getter/setter)

物件屬性可以設定存取器函式,使用get宣告存取器getter函式,set宣告存取器setter函式;若存在存取器函式,則在訪問或設定該屬性時,將呼叫對應的存取器函式:

get

讀取該屬性值時呼叫該函式並將該函式返回值賦值給屬性值;


    var x = {};
    Object.defineProperty(x, 'count', {
        get: function() {
            console.log('讀取count屬性 +1');
            return 0;
        }
    });
    console.log(x); // Object {count: 0}
    x.count = 1;
    // '讀取count屬性 +1'
    console.log(x.count); // 0複製程式碼

set

當設定函式值時呼叫該函式,該函式接收設定的屬性值作引數:


    var x = {};
    Object.defineProperty(x, 'count', {
        set: function(val) {
            this.count = val;
        }
    });
    console.log(x);
    x.count = 1;複製程式碼

執行上訴程式碼,會發現報錯,執行棧溢位:

從 JavaScript 屬性描述器剖析 Vue.js 響應式檢視
棧溢位

上述程式碼在設定count屬性時,會呼叫set方法,而在該方法內為count屬性賦值會再次觸發set方法,所以這樣是行不通的,JavaScript使用另一種方式,通常存取器函式得同時宣告,程式碼如下:


    var x = {};
    Object.defineProperty(x, 'count', {
        get: function() {
            return this._count;
        },
        set: function(val) {
            console.log('設定count屬性 +1');
            this._count = val;
        }
    });
    console.log(x); // Object {count: undefined}
    x.count = 1;
    // '設定count屬性 +1'
    console.log(x.count); 1複製程式碼

事實上,在使用defineProperty()方法設定屬性時,通常需要在物件內部維護一個新內部變數(以下劃線_開頭,表示不希望被外部訪問),作為存取器函式的中介。

注:當設定了存取器描述時,不能設定valuewritable描述。

我們發現,設定屬性存取器函式後,我們可以實現對該屬性的實時監控,這在實踐中很有用武之地,後文會印證這一點。

enumerable

指定物件內某屬性是否可列舉,即使用for in操作是否可遍歷:


    var x = {
        name: 'jh'
    };
    Object.defineProperty(x, 'count', {
        value: 0
    });
    for (var key in x) {
        console.log(key + ' is ' + x[key]);
    }
    // name is jh複製程式碼

上面無法遍歷count屬性,因為使用defineProperty()方法時,預設有enumerable: false,需要顯示宣告該描述:


    var x = {
        name: 'jh'
    };
    Object.defineProperty(x, 'count', {
        value: 0,
        enumerable: true
    });
    for (var key in x) {
        console.log(key + ' is ' + x[key]);
    }
    // name is jh
    // count is 0
    x.propertyIsEnumerable('count'); // true複製程式碼

configurable

該值指定物件屬性描述是否可變:


    var x = {};
    Object.defineProperty(x, 'count', { 
        value: 0, 
        writable: false
    });
    Object.defineProperty(x, 'count', { 
        value: 0, 
        writable: true
    });複製程式碼

執行上述程式碼會報錯,因為使用defineProperty()方法時預設是configurable: false,輸出如圖:

從 JavaScript 屬性描述器剖析 Vue.js 響應式檢視
configurable:false

修改如下,即可:


    var x = {};
    Object.defineProperty(x, 'count', { 
        value: 0, 
        writable: false,
        configurable: true
    });
    x.count = 1;
    console.log(x.count); // 0
    Object.defineProperty(x, 'count', { 
        writable: true
    });
    x.count = 1;
    console.log(x.count); // 1複製程式碼

屬性描述與檢視模型繫結

介紹完屬性描述物件,我們來看看其在現代JavaScript框架和類庫上的應用。目前有很多框架和類庫實現資料和DOM檢視的單向甚至雙向繫結,如React,angular.js,avalon.js,,Vue.js等,使用它們很容易做到對資料變更進行響應式更新DOM檢視,甚至檢視和模型可以實現雙向繫結,同步更新。當然這些框架、類庫內部實現原理主要分為三大陣營。本文以Vue.js為例,Vue.js是當下比較流行的一個響應式的檢視層類庫,其內部實現響應式原理就是本文介紹的屬性描述在技術中的具體應用。

可以點選此處,檢視一個原生JavaScript實現的簡易資料檢視單向繫結例項,在該例項中,點選按鈕可以實現計數自增,在輸入框輸入內容會同步更新到展示DOM,甚至在控制檯改變data物件屬性值,DOM會響應更新,如圖:

從 JavaScript 屬性描述器剖析 Vue.js 響應式檢視
資料檢視單向繫結例項

點選檢視完整例項程式碼

資料檢視單向繫結

現有如下程式碼:


    var data = {};
    var contentEl = document.querySelector('.content');


    Object.defineProperty(data, 'text', {
        writable: true,
        configurable: true,
        enumerable: true,
        get: function() {
            return contentEl.innerHTML;
        },
        set: function(val) {
            contentEl.innerHTML = val;
        }
    });複製程式碼

很容易看出,當我們設定data物件的text屬性時,會將該值設定為檢視DOM元素的內容,而訪問該屬性值時,返回的是檢視DOM元素的內容,這就簡單的實現了資料到檢視的單向繫結,即資料變更,檢視也會更新。

以上僅是針對一個元素的資料檢視繫結,但稍微有經驗的開發者便可以根據以上思路,進行封裝,很容易的實現一個簡易的資料到檢視單向繫結的工具類。

抽象封裝

接下來對以上例項進行簡單抽象封裝,點選檢視完整例項程式碼

首先宣告資料結構:


    window.data = {
        title: '資料檢視單向繫結',
        content: '使用屬性描述器實現資料檢視繫結',
        count: 0
    };
    var attr = 'data-on'; // 約定好的語法,宣告DOM繫結物件屬性複製程式碼

然後封裝函式批量處理物件,遍歷物件屬性,設定描述物件同時為屬性註冊變更時的回撥:


    // 為物件中每一個屬性設定描述物件,尤其是存取器函式
    function defineDescriptors(obj) {
        for (var key in obj) {
            // 遍歷屬性
            defineDescriptor(obj, key, obj[key]);
        }

        // 為特定屬性設定描述物件
        function defineDescriptor(obj, key, val) {
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get: function() {
                     var value = val;
                     return value;
                },
                set: function(newVal) {
                     if (newVal !== val) {
                         // 值發生變更才執行
                         val = newVal;
                         Observer.emit(key, newVal); // 觸發更新DOM
                     }
                }
            });
            Observer.subscribe(key); // 為該屬性註冊回撥
        }
    }複製程式碼

管理事件

以釋出訂閱模式管理屬性變更事件及回撥:


    // 使用釋出/訂閱模式,集中管理監控和觸發回撥事件
    var Observer = {
        watchers: {},
        subscribe: function(key) {
            var el = document.querySelector('[' + attr + '="'+ key + '"]');

            // demo
            var cb = function react(val) {
                el.innerHTML = val;
            }

            if (this.watchers[key]) {
                this.watchers[key].push(cb);
            } else {
                this.watchers[key] = [].concat(cb);
            }
        },
        emit: function(key, val) {
            var len = this.watchers[key] && this.watchers[key].length;

            if (len && len > 0) {
                for(var i = 0; i < len; i++) {
                    this.watchers[key][i](val);
                }
            }
        }
    };複製程式碼

初始化例項

最後初始化例項:


    // 初始化demo
    function init() {
        defineDescriptors(data); // 處理資料物件
        var eles = document.querySelectorAll('[' + attr + ']');

        // 初始遍歷DOM展示資料
        // 其實可以將該操作放到屬性描述物件的get方法內,則在初始化時只需要對屬性遍歷訪問即可
        for (var i = 0, len = eles.length; i < len; i++) {
            eles[i].innerHTML = data[eles[i].getAttribute(attr)];
        }

        // 輔助測試例項
        document.querySelector('.add').addEventListener('click', function(e) {
            data.count += 1;
        });

    }
    init();複製程式碼

html程式碼參考如下:


    <h2 class="title" data-on="title"></h2>
    <div class="content" data-on="content"></div>
    <div class="count" data-on="count"></div>
    <div>
        請輸入內容:
        <input type="text" class="content-input" placeholder="請輸入內容">
    </div>
    <button class="add" onclick="">加1</button>複製程式碼

Vue.js的響應式原理

上一節實現了一個簡單的資料檢視單向繫結例項,現在對Vue.js的響應式單向繫結進行簡要分析,主要需要理解其如何追蹤資料變更。

依賴追蹤

Vue.js支援我們通過data引數傳遞一個JavaScript物件做為元件資料,然後Vue.js將遍歷此物件屬性,使用Object.defineProperty方法設定描述物件,通過存取器函式可以追蹤該屬性的變更,本質原理和上一節例項差不多,但是不同的是,Vue.js建立了一層Watcher層,在元件渲染的過程中把屬性記錄為依賴,之後當依賴項的setter被呼叫時,會通知Watcher重新計算,從而使它關聯的元件得以更新,如下圖:

從 JavaScript 屬性描述器剖析 Vue.js 響應式檢視
Vue.js響應式原理圖

元件掛載時,例項化watcher例項,並把該例項傳遞給依賴管理類,元件渲染時,使用物件觀察介面遍歷傳入的data物件,為每個屬性建立一個依賴管理例項並設定屬性描述物件,在存取器函式get函式中,依賴管理例項新增(記錄)該屬性為一個依賴,然後當該依賴變更時,觸發set函式,在該函式內通知依賴管理例項,依賴管理例項分發該變更給其記憶體儲的所有watcher例項,watcher例項重新計算,更新元件。

因此可以總結說Vue.js的響應式原理是依賴追蹤,通過一個觀察物件,為每個屬性,設定存取器函式並註冊一個依賴管理例項depdep內為每個元件例項維護一個watcher例項,在屬性變更時,通過setter通知dep例項,dep例項分發該變更給每一個watcher例項,watcher例項各自計算更新元件例項,即watcher追蹤dep新增的依賴,Object.defineProperty()方法提供這種追蹤的技術支援,dep例項維護這種追蹤關係。

原始碼簡單分析

接下來對Vue.js原始碼進行簡單分析,從對JavaScript物件和屬性的處理開始:

觀察物件(Observer)

首先,Vue.js也提供了一個抽象介面觀察物件,為物件屬性設定儲存器函式,收集屬性依賴然後分發依賴更新:


    var Observer = function Observer (value) {
          this.value = value;
          this.dep = new Dep(); // 管理物件依賴
          this.vmCount = 0;
          def(value, '__ob__', this); // 快取處理的物件,標記該物件已處理
          if (Array.isArray(value)) {
            var augment = hasProto
              ? protoAugment
              : copyAugment;
            augment(value, arrayMethods, arrayKeys);
            this.observeArray(value);
          } else {
            this.walk(value);
          }
    };複製程式碼

上面程式碼關注兩個節點,this.observeArray(value)this.walk(value);

  1. 若為物件,則呼叫walk()方法,遍歷該物件屬性,將屬性轉換為響應式:

    
         Observer.prototype.walk = function walk (obj) {
               var keys = Object.keys(obj);
               for (var i = 0; i < keys.length; i++) {
                 defineReactive$$1(obj, keys[i], obj[keys[i]]);
               }
         };複製程式碼

    可以看到,最終設定屬性描述物件是通過呼叫defineReactive$$1()方法。

  2. 若value為物件陣列,則需要額外處理,呼叫observeArray()方法對每一個物件均產生一個Observer例項,遍歷監聽該物件屬性:

    
         Observer.prototype.observeArray = function observeArray (items) {
               for (var i = 0, l = items.length; i < l; i++) {
                 observe(items[i]);
               }
         };複製程式碼

    核心是為每個陣列項呼叫observe函式:

    
     function observe(value, asRootData) {
         if (!isObject(value)) {
             return // 只需要處理物件
         }
         var ob;
         if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
             ob = value.__ob__; // 處理過的則直接讀取快取
         } else if (
             observerState.shouldConvert &&
             !isServerRendering() &&
             (Array.isArray(value) || isPlainObject(value)) &&
             Object.isExtensible(value) &&
             !value._isVue) {
                ob = new Observer(value); // 處理該物件
         }
         if (asRootData && ob) {
             ob.vmCount++;
         }
         return ob
     }複製程式碼

    呼叫ob = new Observer(value);後就回到第一種情況的結果:呼叫defineReactive$$1()方法生成響應式屬性。

生成響應式屬性

原始碼如下:


    function defineReactive$$1 (obj,key,val,customSetter) {
          var dep = new Dep(); // 管理屬性依賴

          var property = Object.getOwnPropertyDescriptor(obj, key);
          if (property && property.configurable === false) {
            return
         }

         // 之前已經設定了的get/set需要合併呼叫
          var getter = property && property.get; 
          var setter = property && property.set;

          var childOb = observe(val); // 屬性值也可能是物件,需要遞迴觀察處理
          Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter () {
                  var value = getter ? getter.call(obj) : val;
                  if (Dep.target) { // 管理依賴物件存在指向的watcher例項
                    dep.depend(); // 新增依賴(記錄)
                    if (childOb) { // 屬性值為物件
                          childOb.dep.depend(); // 屬性值物件也需要新增依賴
                    }
                    if (Array.isArray(value)) {
                          dependArray(value); // 處理陣列
                    }
                  }
                  return value
            },
            set: function reactiveSetter (newVal) {
                  var value = getter ? getter.call(obj) : val;
                  /* eslint-disable no-self-compare */
                  if (newVal === value || (newVal !== newVal && value !== value)) {
                return // 未發生變更不需要往後執行
                  }
                  /* eslint-enable no-self-compare */
                  if ("development" !== 'production' && customSetter) {
                    customSetter();
                  }
                  if (setter) {
                    setter.call(obj, newVal); // 更新屬性值
                  } else {
                    val = newVal; // 更新屬性值
                  }
                  childOb = observe(newVal); // 每次值變更時需要重新觀察,因為可能值為物件
                  dep.notify(); // 釋出更新事件
            }
         });
    }複製程式碼

該方法使用Object.defineProperty()方法設定屬性描述物件,邏輯集中在屬性存取器函式內:

  1. get: 返回屬性值,如果watcher存在,則遞迴記錄依賴;
  2. set: 屬性值發生變更時,更新屬性值,並呼叫dep.notify()方法釋出更新事件;

管理依賴

Vue.js需要管理物件的依賴,在屬性更新時通知watcher更新元件,進而更新檢視,Vue.js管理依賴介面採用釋出訂閱模式實現,原始碼如下:


    var uid$1 = 0;
    var Dep = function Dep () {
          this.id = uid$1++; // 依賴管理例項id
          this.subs = []; // 訂閱該依賴管理例項的watcher例項陣列
    };
    Dep.prototype.depend = function depend () { // 新增依賴
          if (Dep.target) {
                Dep.target.addDep(this); // 呼叫watcher例項方法訂閱此依賴管理例項
          }
    };
    Dep.target = null; // watcher例項
    var targetStack = []; // 維護watcher例項棧

    function pushTarget (_target) {
          if (Dep.target) { targetStack.push(Dep.target); }
          Dep.target = _target; // 初始化Dep指向的watcher例項
    }

    function popTarget () {
          Dep.target = targetStack.pop();
    }複製程式碼
訂閱

如之前,生成響應式屬性為屬性設定存取器函式時,get函式內呼叫dep.depend()方法新增依賴,該方法內呼叫Dep.target.addDep(this);,即呼叫指向的watcher例項的addDep方法,訂閱此依賴管理例項:


    Watcher.prototype.addDep = function addDep (dep) {
          var id = dep.id;
          if (!this.newDepIds.has(id)) { // 是否已訂閱
            this.newDepIds.add(id); // watcher例項維護的依賴管理例項id集合
            this.newDeps.push(dep); // watcher例項維護的依賴管理例項陣列
            if (!this.depIds.has(id)) { // watcher例項維護的依賴管理例項id集合
                // 呼叫傳遞過來的依賴管理例項方法,新增此watcher例項為訂閱者
                  dep.addSub(this); 
            }
          }
    };複製程式碼

watcher例項可能同時追蹤多個屬性(即訂閱多個依賴管理例項),所以需要維護一個陣列,儲存多個訂閱的依賴管理例項,同時記錄每一個例項的id,便於判斷是否已訂閱,而後呼叫依賴管理例項的addSub方法:


    Dep.prototype.addSub = function addSub (sub) {
          this.subs.push(sub); // 實現watcher到依賴管理例項的訂閱關係
    };複製程式碼

該方法只是簡單的在訂閱陣列內新增一個訂閱該依賴管理例項的watcher例項。

釋出

屬性變更時,在屬性的存取器set函式內呼叫了dep.notify()方法,釋出此屬性變更:


    Dep.prototype.notify = function notify () {
          // 複製訂閱者陣列
          var subs = this.subs.slice();
          for (var i = 0, l = subs.length; i < l; i++) {
            subs[i].update(); // 分發變更
          }
    };複製程式碼

觸發更新

前面提到,Vue.js中由watcher層追蹤依賴變更,發生變更時,通知元件更新:


    Watcher.prototype.update = function update () {
          /* istanbul ignore else */
          if (this.lazy) {
            this.dirty = true;
          } else if (this.sync) { // 同步
            this.run();
          } else { // 非同步
            queueWatcher(this); // 最後也是呼叫run()方法
          }
    };複製程式碼

呼叫run方法,通知元件更新:


    Watcher.prototype.run = function run () {
          if (this.active) {
            var value = this.get(); // 獲取新屬性值
            if (value !== this.value || // 若值
                isObject(value) || this.deep) {
                  var oldValue = this.value; // 快取舊值
                  this.value = value; // 設定新值
                  if (this.user) {
                    try {
                          this.cb.call(this.vm, value, oldValue);
                    } catch (e) {
                          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
                    }
                  } else {
                        this.cb.call(this.vm, value, oldValue);
                  }
            }
          }
    };複製程式碼

呼叫this.get()方法,實際上,後面會看到在該方法內處理了屬性值的更新與元件的更新,這裡判斷當屬性變更時呼叫初始化時傳給例項的cb回撥函式,並且回撥函式接受屬性新舊值兩個引數,此回撥通常是對於watch宣告的監聽屬性才會存在,否則預設為空函式。

追蹤依賴介面例項化

每一個響應式屬性都是由一個Watcher例項追蹤其變更,而針對不同屬性(data, computed, watch),Vue.js進行了一些差異處理,如下是介面主要邏輯:


    var Watcher = function Watcher (vm,expOrFn,cb,options) {
        this.cb = cb;
        ...
        // parse expression for getter
          if (typeof expOrFn === 'function') {
            this.getter = expOrFn;
          } else {
                this.getter = parsePath(expOrFn);
          }
        this.value = this.lazy
            ? undefined
            : this.get();
    };複製程式碼

在初始化Watcher例項時,會解析expOrFn引數(表示式或者函式)成擴充getterthis.getter,然後呼叫this.get()方法,返回值作為this.value值:


    Watcher.prototype.get = function get () {
          pushTarget(this); // 入棧watcher例項
          var value;
          var vm = this.vm;
          if (this.user) {
            try {
                  value = this.getter.call(vm, vm); // 通過this.getter獲取新值
            } catch (e) {
                  handleError(e, vm, ("getter for watcher \"" +
 (this.expression) + "\""));
            }
          } else {
            value = this.getter.call(vm, vm); // 通過this.getter獲取新值
          }

          if (this.deep) { // 深度遞迴遍歷物件追蹤依賴
            traverse(value);
          }
          popTarget(); // 出棧watcher例項
          this.cleanupDeps(); // 清空快取依賴
          return value // 返回新值
    };複製程式碼

這裡需要注意的是對於data屬性,而非computed屬性或watch屬性,而言,其watcher例項的this.getter通常就是updateComponent函式,即渲染更新元件,get方法返回undefined,而對於computed計算屬性而言,會傳入對應指定函式給this.getter,其返回值就是此get方法返回值。

data普通屬性

Vue.jsdata屬性是一個物件,需要呼叫物件觀察介面new Observer(value)


    function observe (value, asRootData) {
        if (!isObject(value)) {
            return
          }
          var ob;
        ob = new Observer(value); // 物件觀察例項
        return ob;
    }

    // 初始處理data屬性
    function initData (vm) {
        // 呼叫observe函式
        observe(data, true /* asRootData */);
    }複製程式碼
計算屬性

Vue.js對計算屬性處理是有差異的,它是一個變數,可以直接呼叫Watcher介面,把其屬性指定的計算規則傳遞為,屬性的擴充getter,即:


    // 初始處理computed計算屬性
    function initComputed (vm, computed) {
        for (var key in computed) {
            var userDef = computed[key]; // 對應的計算規則
            // 傳遞給watcher例項的this.getter -- 擴充getter
            var getter = typeof userDef === 'function' ? 
                userDef : userDef.get; 
            watchers[key] = new Watcher(vm, 
                getter, noop, computedWatcherOptions);
        }
    }複製程式碼
watch屬性

而對於watch屬性又有不同,該屬性是變數或表示式,而且與計算屬性不同的是,它需要指定一個變更事件發生後的回撥函式:


    function initWatch (vm, watch) {
        for (var key in watch) {
            var handler = watch[key];
            createWatcher(vm, key, handler[i]); // 傳遞迴調
        }
    }
    function createWatcher (vm, key, handler) {
        vm.$watch(key, handler, options); // 回撥
    }
    Vue.prototype.$watch = function (expOrFn, cb, options) {
        // 例項化watcher,並傳遞迴調
        var watcher = new Watcher(vm, expOrFn, cb, options);
    }複製程式碼
初始化Watcher與依賴管理介面的連線

無論哪種屬性最後都是由watcher介面實現追蹤依賴,而且元件在掛載時,即會初始化一次Watcher例項,繫結到Dep.target,也就是將WatcherDep建立連線,如此在元件渲染時才能對屬性依賴進行追蹤:

    function mountComponent (vm, el, hydrating) {
        ...
        updateComponent = function () {
              vm._update(vm._render(), hydrating);
            ...
        };
        ...
        vm._watcher = new Watcher(vm, updateComponent, noop);
        ...
    }複製程式碼

如上,傳遞updateComponent方法給watcher例項,該方法內觸發元件例項的vm._render()渲染方法,觸發元件更新,此mountComponent()方法會在$mount()掛載元件公開方法中呼叫:


    // public mount method
    Vue$3.prototype.$mount = function (el, hydrating) {
          el = el && inBrowser ? query(el) : undefined;
          return mountComponent(this, el, hydrating)
    };複製程式碼

總結

到此為止,對於JavaScript屬性描述器介面的介紹及其應用,還有其在Vue.js中的響應式實踐原理基本闡述完了,這次總結從原理到應用,再到實踐剖析,花費比較多精力,但是收穫是成正比的,不僅對JavaScript基礎有更深的理解,還更熟悉了Vue.js響應式的設計原理,對其原始碼熟悉度也有較大提升,之後在工作和學習過程中,會進行更多的總結分享。

參考

  1. Object.defineProperty
  2. Vue.js Reactivity in Depth
  3. Object Initializer

原創文章,轉載請註明: 轉載自 熊建剛的部落格

本文連結地址: 從JavaScript屬性描述器剖析Vue.js響應式檢視

相關文章