深入瞭解Vue響應式系統

前言

前面幾篇文章一直都以源碼分析爲主,其實枯燥無味,對於新手玩家來說很不友好。這篇文章主要講講 Vue 的響應式系統,形式與前邊的稍顯
不同吧,分析爲主,源碼爲輔,如果能達到深入淺出的效果那就更好了。

什麼是響應式系統

「響應式系統」一直以來都是我認爲 Vue 裏最核心的幾個概念之一。想深入理解 Vue ,首先要掌握「響應式系統」的原理。

從一個官方的例子開始

由於 Vue 不允許動態添加根級響應式屬性,所以你必須在初始化實例前聲明所有根級響應式屬性,哪怕只是一個空值:

var vm = new Vue({
  data: {
    // 聲明 message 爲一個空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之後設置 `message`
vm.message = 'Hello!'
如果你未在 data 選項中聲明 message,Vue 將警告你渲染函數正在試圖訪問不存在的屬性。

當然,僅僅從上面這個例子我們也只能知道,Vue不允許動態添加根級響應式屬性。這意味我們需要將使用到的變量先在data函數中聲明。

拋磚🧱引玉

新建一個空白工程,加入以下代碼

export default {
    name: 'JustForTest',
    data () {
        return {}
    },
    created () {
        this.b = 555
        console.log(this.observeB)
        this.b = 666
        console.log(this.observeB)
    },
    computed: {
        observeB () {
            return this.b
        }
    }
}

運行上述代碼,結果如下:

555
555

在上面的代碼中我們做了些什麼?

  1. 沒有在 data 函數中聲明變量(意味着此時沒有根級響應式屬性)
  2. 定義了一個 computed 屬性 —— observeB ,用來返回(監聽)變量b
  3. 使用了變量 b 同時賦值 555 ,打印 this.observeB
  4. 使用了變量 b 同時賦值 666 ,打印 this.observeB

打印結果爲什麼都是555

有段簡單的代碼可以解釋這個原因:

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}
...
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

createComputedGetter函數返回一個閉包函數並掛載在computed屬性的getter上,一旦觸發computed屬性的getter
那麼就會調用computedGetter

顯然,輸出 555 是因爲觸發了 this.observeBgetter ,從而觸發了 computedGetter ,最後執行 Watcher.evalute()
然而,決定 watcher.evalute() 函數執行與否與 watcherwatcher.dirty 的值是否爲空有關

深入瞭解響應式系統

Object.defineProperty

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。

那麼這個函數應該怎麼使用呢?給個官方的源碼當做例子:

function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}
def(value, '__ob__', this);

gettersetter

上面提到了 Object.defineProperty 函數,其實這個函數有個特別的參數 —— descriptor(屬性描述符),簡單看下MDN
上的定義:

對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個具有值的屬性,該值可能是可寫的,也可能不是
可寫的。存取描述符是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是兩者。

其中需要特別提到的就是 gettersetter,在 descriptor(屬性描述符)中分別代表 get 方法和 set
方法

get

一個給屬性提供 getter 的方法,如果沒有 getter 則爲 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,
但是會傳入this對象(由於繼承關係,這裏的this並不一定是定義該屬性的對象)。

set

一個給屬性提供 setter 的方法,如果沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受唯一參數,
即該屬性新的參數值。

小結

  1. 對象在被訪問時會觸發getter
  2. 對象在被賦值是會觸發setter
  3. 利用getter我們可以知道哪些對象被使用了
  4. 利用setter我們可以知道哪些對象被賦值了

依賴收集

Vue基於Object.defineProperty函數,可以對變量進行依賴收集,從而在變量的值改變時觸發視圖的更新。簡單點來講就是:
Vue需要知道用到了哪些變量,不用的變量就不管,在它(變量)變化時,Vue就通知對應綁定的視圖進行更新。
舉個例子:

 Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        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 (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) { return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });

這段代碼做了哪些事情呢?主要有以下幾點:

  • 對於 obj[key],定義它的 getset 函數
  • obj[key] 被訪問時,觸發 get 函數,調用 dep.depend 函數收集依賴
  • obj[key] 被賦值時,調用 set 函數,調用 dep.notify 函數觸發視圖更新

如果你再深入探究下去,那麼還會發現 dep.notify 函數裏還調用了 update 函數,而它恰好就是 Watcher 類所屬
的方法,上面所提到的 computed 屬性的計算方法也恰好也屬於 Watcher

Observer

前面所提到的 Object.defineProperty 函數到底是在哪裏被調用的呢?答案就是 initData 函數和 Observer類。
可以歸納出一個清晰的調用邏輯:

  • 初始化 data 函數,此時調用 initData 函數
  • 在調用 initData 函數時,執行 observe 函數,這個函數執行成功後會返回一個 ob 對象
  • observe 函數返回的 ob 對象依賴於 Observer 函數
  • Observer 分別對對象和數組做了處理,對於某一個屬性,最後都要執行 walk 函數
  • walk 函數遍歷傳入的對象的 key 值,對於每個 key 值對應的屬性,依次調用 defineReactive$$1 函數
  • defineReactive$$1 函數中執行 Object.defineProperty 函數
  • ...

感興趣的可以看下主要的代碼,其實邏輯跟上面描述的一樣,只不過步驟比較繁瑣,耐心閱讀源碼的話還是能看懂。

initData

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    ...
  }
  // proxy data on instance
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    ...
    if (props && hasOwn(props, key)) {
        ...
    } else if (!isReserved(key)) {
      proxy(vm, "_data", key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

observe

function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

Observer

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

更加方便的定義響應式屬性

文檔中提到,Vue 建議在根級聲明變量。通過上面的分析我們也知道,在 data 函數中
聲明變量則使得變量變成「響應式」的,那麼是不是所有的情況下,變量都只能在 data 函數中
事先聲明呢?

$set

Vue 其實提供了一個 $set 的全局函數,通過 $set 就可以動態添加響應式屬性了。

export default {
    data () {
        return {}
    },
    created () {
        this.$set(this, 'b', 666)
    },
}

然而,執行上面這段代碼後控制檯卻報錯了
<font color=Red> [Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option. </font>

其實,對於已經創建的實例,Vue 不允許動態添加根級別的響應式屬性。
$set 函數的執行邏輯:

  • 判斷實例是否是數組,如果是則將屬性插入
  • 判斷屬性是否已定義,是則賦值後返回
  • 判斷實例是否是 Vue 的實例或者是已經存在 ob 屬性(其實也是判斷了添加的屬性是否屬於根級別的屬性),是則結束函數並返回
  • 執行 defineReactive$$1,使得屬性成爲響應式屬性
  • 執行 ob.dep.notify(),通知視圖更新

相關代碼:

function set (target, key, val) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
  }
  var ob = (target).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    );
    return val
  }
  if (!ob) {
    target[key] = val;
    return val
  }
  c(ob.value, key, val);
  ob.dep.notify();
  return val
}

數組操作

爲了變量的響應式,Vue 重寫了數組的操作。其中,重寫的方法就有這些:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

那麼這些方法是怎麼重寫的呢?
首先,定義一個 arrayMethods 繼承 Array

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

然後,利用 object.defineProperty,將 mutator 函數綁定在數組操作上:

def(arrayMethods, method, function mutator () { ... })

最後在調用數組方法的時候,會直接執行 mutator函數。源碼中,對這三種方法做了特別
處理:

  • push
  • unshift
  • splice

因爲這三種方法都會增加原數組的長度。當然如果調用了這三種方法,會再調用一次 observeArray
方法(這裏的邏輯就跟前面提到的一樣了)
最後的最後,調用 notify 函數

核心代碼:

methodsToPatch.forEach(function (method) {
 // cache original method
 var original = arrayProto[method];
 def(arrayMethods, method, function mutator () {
   var args = [], len = arguments.length;
   while ( len-- ) args[ len ] = arguments[ len ];

   var result = original.apply(this, args);
   var ob = this.__ob__;
   var inserted;
   switch (method) {
     case 'push':
     case 'unshift':
       inserted = args;
       break
     case 'splice':
       inserted = args.slice(2);
   }
   if (inserted) { ob.observeArray(inserted); }
   // notify change
   ob.dep.notify();
   return result
 });
});

總結

「響應式原理」藉助了這三個類來實現,分別是:

  • Watcher
  • Observer
  • Dep

初始化階段,利用 getter 的特點,監聽到變量被訪問 ObserverDep 實現對變量的「依賴收集」,
賦值階段利用 setter 的特點,監聽到變量賦值,利用 Dep 通知 Watcher,從而進行視圖更新。
avatar

參考資料

深入響應式原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章