深刻了解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函數中聲明。javascript

拋磚🧱引玉

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

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

有段簡單的代碼能夠解釋這個緣由:vue

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
那麼就會調用computedGetterjava

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

深刻了解響應式系統

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就通知對應綁定的視圖進行更新。
舉個例子:app

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

參考資料

深刻響應式原理

掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。

相關文章
相關標籤/搜索