Vue2.x雙向數據綁定原理-Array篇

前言

在上一篇Vue雙向數據綁定-Object篇說過,Vue中Array和Object的雙向數據綁定是不同的。當使用數組的方法去改變數據,是觸發不了setter的,那就是無法通setter去通知依賴了,那麼Vue是怎樣對數組的改變進行監控的呢?javascript

由於Array的實現依然須要用到defineReactiveDepWatcherObserver,這些方法或類的已經在上一篇大體實現了,不瞭解的建議先看完Vue雙向數據綁定-Object篇java

基礎知識

開始以前仍是先來點基礎知識吧數組

改變數組的方法

要監控數據的變化,是否是要先知道那些方法會改變原數組呢?具體以下瀏覽器

  • push:向數組後面添加
  • pop:從數組最後刪除
  • unshift:從數組前面添加
  • shift:從數組前面刪除
  • splice:刪除、替換或添加
  • sort:排序
  • reverse:位置顛倒

想要監聽數組的變化,當被監測的數組調用上面所說的方法,再去通知數組的依賴是否是就實現了對數組的監控了呢?固然這裏說的只是當前Vue 2.x實現的數組監測,當this.arr[0] = 3這樣去改變數組的時候依然是沒辦法監測獲得的,這個的完善就要等Vue 3了。app

原型鏈

原型鏈這個知識點要講明白,篇幅要挺長的,請自行查閱資料繼承與原型鏈。這裏簡單放個例子體現函數

var arr = [1, 2, 3]
arr.push = function(item) {
  arr[arr.length] = 'push';
}
arr.push(4);
console.log(arr); // 輸出 [1, 2, 3, 'push', push: f]
複製代碼

這時arr.push調的是本身定義的方法而不是Array.prototype上的push方法。工具

攔截器

攔截器其實就是Vue對數組的 ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']方法進行了重寫,在執行Array.prototype的原生方法前,先去作一些Vue須要它作的功能以後在執行。post

const arrayProto = Array.prototype; // 原生的 Array.prototype
const arrayMethods = Object.create(arrayProto); // 拷貝
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
  const original = arrayProto[method];
  // 改寫arrayMethods中的 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' 方法
  Object.defineProperty(arrayMethods, method, { 
    value: function(...args) {
      // TODO 在這裏實現Vue須要作的功能
      return original.apply(this, ...args);
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})
複製代碼

arrayMethods是咱們改寫過的Array.prototype,那麼在把數據變成響應式的時候,把數組的原型方法改爲咱們本身實現的arrayMethods。當觸發這些方法時,就能夠去通知依賴數據發生改變了,進行依賴觸發吧。ui

數組變成響應式數據

前面Object篇是經過Observer變成響應式數據的,那麼就在Observer加上數組的代碼this

const hasProto = '_proto_' in {}; // 判斷瀏覽器可否訪問原型
class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) { // 數組處理
      if (hasProto) { // 瀏覽器兼容原型 把 _proto_ 改寫成本身的 arrayMethods
        value._proto_ = arrayMethods;
      } else { // 不支持就把方法賦值到數組的屬性上
        const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
        for (let i = 0, l = arrayKeys.length; i < l; i++) {
          const key = arrayKeys[i];
          value[key] = arrayMethods[key];
        }
      }
    } else { // 對象處理
      this.walk(value);
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReact(obj, key, obj[key]);
    })
  }
}
複製代碼

收集依賴

數組的依賴收集相對於對象的會複雜些。這裏先舉例子強調一下

{
  arr: [1, 2]
}
複製代碼

data裏有個數組arr,當使用時經過this.arr是否是一樣也會觸發到getter,那麼就能夠在這裏進行依賴收集,可是以前的依賴是收集在defineReactive中的dep實例中,攔截器訪問不到函數內部的dep就觸發不了依賴。

Vue.js是把Array的依賴收集在Observer

// 工具方法
function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
// Observer
class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    // 把Observer實例邦到 value 的 _ob_ 屬性下,那麼每一個響應式數據都有 _ob_ 這個屬性
    // 攔截器就能夠經過 _ob_ 找到依賴了
    def(value, '_ob_', this)
    .......
  }
  .......
}
複製代碼

上面只實現了把依賴收集在哪這一步。前面也說到了Array也是在getter中收集的

// 工具方法
function observe(value, asRootData) { // 獲取Observer實例
  if (!isObject(value)) {
    return;
  }
  let ob;
  if (hasOwn(value, '_ob_') && value._ob_ instanceof Observer) { // 已經被監測直接返回
    ob = value._ob_
  } else { // 還沒監測
    ob = new Observer(value);
  }
  return ob;
}
// defineReactive
function defineReactive(data, key, val) {
  let childOb = observe(value);
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.depend();
      if (childOb) { // 新增
        childOb.dep.depend(); // 依賴收集
      }
      return val;
    },
    set: function(newVal) {
      if (val === newVal) {
        return;
      }
      dep.notify();
      val = newVal;
    }
  })
}
複製代碼

這裏就是增長了代碼把依賴收集到Observer實例中的dep,實際上加了這一步,每一個被監測的數據上都有_ob_屬性。這裏留下個問題去思考下「爲何要不直接都用childOb.dep,還多用了一個dep?」

觸發依賴

上一節實現了依賴的收集,如今要在攔截器實現依賴觸發了

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
    const original = arrayProto[method];
    // 利用上面的工具方法 def 改寫一下
    def(arrayMethods, method, function mutator(...args) {
      const result = original.apply(this, args);
      const od = this._ob_; // 獲取 Observer實例
      ob.dep.notify(); // 觸發依賴
      return result;
    });
  })
複製代碼

那麼到如今這一步數組的監測已經算是完成了,但這僅僅是對數組自己的實現,數組裏的子元素是對象和新增的元素一樣也要把它變成響應式數據,接下來就完善這些操做。

監測子元素

應該都很快想到用遞歸去實現,繼續改寫Observer

class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    def(value, '_ob_', this);

    if (Array.isArray(value)) {
      if (hasProto) {
        value._proto_ = arrayMethods;
      } else {
        const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
        for (let i = 0, l = arrayKeys.length; i < l; i++) {
          const key = arrayKeys[i];
          value[key] = arrayMethods[key];
        }
      }
      this.observeArray(value); // 遞歸監測數組的每一項
    } else {
      this.walk(value);
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReact(obj, key, obj[key]);
    })
  }
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]); // observe 工具方法
    }
  }
}
複製代碼

Observer類上增長了observeArray方法,這樣就能夠在攔截器上經過_ob_來調用。

監測新增元素

在上面說的數組方法中有pushunshiftsplice是能夠添加元素的,就要在攔截器上對這些新增元素來讓它變成響應式數據。

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, ...args);
    const ob = this._ob_;
    let inserted;
    switch (method) {
      case: 'push':
      case: 'unshift':
        inserted = args;
        break;
      case: 'splice': 
        inserted = args.splice(2);
        break;
    }
    if (inserted) {
      ob.observeArray(inserted); // 監測新增元素
    }
    ob.dep.notify();
    return result;
  })
})
複製代碼

到這裏數組自己、數組子元素和新增數組元素的監測已經所有實現,原理的掌握並不難,就是依賴這部份內容相對對象的處理來講複雜一點,也有點繞,看多幾遍就能搞懂了。

總結

Array的監測經過getter和攔截器來實現,在getter中收集依賴,在攔截器中觸發依賴。依賴收集在Observer使攔截器可以調用。

相關文章
相關標籤/搜索