數據響應式初探

近些年來,一股 MVVM 之風颳遍全球,你們無不爲之稱讚。關於 MVVM 架構模式的實現,你們討論的最多的算是 Vue.js 了吧!Vue.js 很好的利用 MVVM 中的 VM 聲明式的實現了與數據模型 Model 和視圖 View 的聯通,使得用戶只需對數據模型進行操做,就能響應到對應的視圖上,這其中核心的實現就是「響應式系統」了。「響應式系統」在整個系統中起到了舉足輕重的做用,爲咱們大大減輕了生產力,瞭解其原理與實現就成爲了咱們技術人無盡的追求,一樣也有助於咱們在實際的生產開發中更好的解決相關的問題。javascript

MVVM

WX20190608-151021@2x.png
上面給出了一張比較簡單可是很形象的圖,它描述了 MVVM 中視圖 View、視圖模型 VM 和數據模型 Model 這三者之間的關係:視圖模型 VM 全稱 ViewModel,它是整個模式的核心,牢牢聯繫着視圖 View 和數據模型 Model。視圖模型 VM 會經過 DOM 事件監聽(DOM Listeners)將視圖數據轉換成數據模型數據,經過數據綁定(Data Bindings)將數據模型轉換成視圖數據以更新視圖。其中的視圖 View 就是咱們熟知的組件頁面了,而數據模型 Model 就是 JavaScript 對象了。

從上面的分析,不難發現視圖 View 和數據模型 Model 算是咱們最熟悉的了,不須要作過多的說明,可是 VM 視圖模型就是咱們須要深挖的了,它的原理實現對整個 MVVM 模型系統很是重要。接下來,大部份內容就是對這個核心的探討了。html

Object.defineProperty

在詳細介紹這個 Object.defineProperty 以前,咱們先拋出幾個問題:vue

  1. Object.defineProperty 是什麼,它能夠實現什麼?
  2. Vue.js 是如何利用它實現響應式系統的?
  3. 利用 Object.defineProperty 實現的響應式系統有什麼缺陷?

問題一

ECMAS-262 第5版在定義只有內部採用的特性時,提供了描述屬性特徵的幾種屬性。ECMAScript 對象中目前存在的屬性描述符主要有兩種:數據描述符(數據屬性)和存取描述符(訪問器屬性)。數據描述符是一個擁有可寫或不可寫值的屬性,存取描述符是由一對 getter-setter 函數功能來描述的屬性。java

Object.defineProperty 就是用來定義對象屬性的屬性描述符方法,它會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。api

數據描述符

數據描述符可包含下列的屬性:數組

  • configurable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。數據結構

  • enumerable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性纔可以出如今對象的枚舉屬性中,好比 for-in循環或 Object.keys() 等。架構

  • writable:默認爲 false。當且僅當該屬性值爲 true 時,value 才能被賦值運算符改變。ide

  • value:該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined。函數

示例代碼:

var obj = {}

Object.defineProperty(obj, 'a', {
  value: 1
})

// 獲取屬性 a 的值:obj.a => 1
// 獲取 obj 上可遍歷的屬性:Object.keys(obj) => []
// 刪除 obj 上的屬性 a:delete obj.a => false
// 從新定義 obj 上的屬性 a:Object.defineProperty(obj, 'a', { value: 1, configurable: true }) => Cannot redefine property 直接報錯
// 從新賦值 obj 上的屬性 a 的值爲 2:obj.a = 2
// 從新獲取屬性 a 的值:obj.a => 1 ⚠️注意:從新賦值並無成功
複製代碼

上面的示例代碼能夠說明:當經過 Object.defineProperty 爲對象定義或修改屬性時,默認不指定屬性描述符,全部的屬性描述符的值都是 false,這會致使該屬性不會存在於對象可遍歷的屬性列表中,不能對屬性從新配置(包括刪除和從新定義屬性),還有對該屬性的從新賦值也不會成功。若是顯式的將這些描述符設置爲 true,那麼以上描述的全部不可行的操做都會可行(能夠被從新賦值,能夠被刪除,能夠被從新定義,能夠被遍歷等),這裏我就不演示了,你們感興趣能夠實操一下。

存取描述符

存取描述符可包含下列的屬性:

  • configurable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。

  • enumerable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性纔可以出如今對象的枚舉屬性中,好比 for-in循環或 Object.keys() 等。

  • get:一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,可是會傳入 this 對象(因爲繼承關係,這裏的 this 並不必定是定義該屬性的對象)。默認爲 undefined。

  • set:一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受惟一參數,即該屬性新的參數值。默認爲 undefined。

示例代碼:

var obj = {}
var val = ''

Object.defineProperty(obj, 'a', {
  get() {
    return val
  },
  set(newVal) {
    val = newVal
  }
})
複製代碼

image.png

當你運行上面的示例代碼,你會發現 obj 對象變成了上面這樣,是否是有種很熟悉的感受?對,在 Vue.js 項目中,咱們無時不刻不見到這樣的數據結構。這就是 Object.defineProperty 存取描述符的魅力了:它會給定義過的屬性添加 set 和 get 方法。當咱們經過 . 符號或者 [] 給對象的屬性賦值時,就會觸發 set 方法了;當咱們經過 . 符號或者 [] 獲取對象的屬性的對應值時,就會觸發 get 方法了。正由於 Object.defineProperty 有這樣一個能力,因此咱們能夠經過它實現響應式系統,完成 MVVM 模式中 VM 這重要的一環。

Object.defineProperty 的存取描述符中依然能夠包含 configurableenumerable,這兩個屬性的做用咱們在數據描述符中已經提到過了,這裏就再也不贅述了。

問題二

上面咱們已經分析過了,咱們能夠經過 Object.defineProperty 存取描述符將對象定義成可觀察的,而後在 set 和 get 方法中加上對應的邏輯。

爲了便於看到效果,這裏先定義一個方法,該方法在調用時會輸出「視圖更新啦~~」

function cb() {
  console.log('視圖更新啦~~')
}
複製代碼

爲了實現更好的複用和便於遞歸,咱們將定義一個名爲 defineReactive 的方法,該方法就是對 Object.defineProperty 邏輯的封裝,它會接受對象、須要定義的屬性 key 值和屬性的值,而後根據這些參數定義屬性的 getter、setter 方法,實現響應式。

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,       	// 屬性可枚舉
    configurable: true,     	// 屬性可被修改或刪除
    get() {
      return val       
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      observer(val)
      cb(newVal);
    }
  })
}
複製代碼

要想將數據變成深度可觀察的,咱們還須要封裝一層。封裝的邏輯主要是對類型進行判斷,而後就是對深層的屬性進行遍歷並調用 defineReactive 實現徹底數據響應式。

function observer(value) {
  if (!value || (typeof value !== 'object')) {
    return
  }

  Object.keys(value).forEach((key) => {
    const val = value[key]
    observer(val)
    defineReactive(value, key, val);
  })
}
複製代碼

到了這一步,就來測試一下咱們的成果吧:

// 定義一個 obj 多級嵌套對象
var obj = {a: 1, b: { c: 2 }}

// 將 obj 變成可觀察的
observer(obj)

obj.b = 'lane'   =>  視圖更新啦~~
複製代碼

成果還不錯,咱們就來趁熱打鐵封裝一個簡單的 Vue 響應式系統吧!先來看一個最簡單的 Vue 使用示例:

const vm = new Vue({
  data: {
    message: 'I am lane.'
  }
})
複製代碼

Vue 會做爲構造函數進行調用並接受一個對象做爲函數。目前在最簡單的狀況下,參數對象只包含一個 data 屬性,咱們的目的就是將這個 data 屬性值變成可觀察的。

// Vue構造類
class Vue {
  constructor(options) {
    this._data = options.data;
    observer(this._data);
  }
}
複製代碼

測試簡易封裝的 Vue 示例代碼:

vm._data.message = 'hello, world.'  // 視圖更新啦~
複製代碼

固然這還只是 Vue.js 中響應式系統的第一步,爲了更好的進行數據更新處理,系統還須要進行依賴收集,以確保數據更新性能達到更優。

問題三

經過 Object.defineProperty 實現的數據響應式邏輯對於數組的許多方法都不能觸發 set 方法(包括 push、pop、shift、unshift、splice、sort、reverse),Vue.js 爲了解決這個問題,從新包裝了這些函數,同時當這些方法被調用的時候,手動去觸發更新操做;還有另外一個問題,官網也有特別的指出

因爲 JavaScript 的限制,Vue 不能檢測如下變更的數組:

  • 當你利用索引直接設置一個項時,例如:vm.items[indexOfItem] = newValue
  • 當你修改數組的長度時,例如:vm.items.length = newLength

這個最根本的緣由是由於這兩種狀況下,受制於js自己沒法實現監聽,因此官方建議用他們本身提供的內置 api 來實現,咱們也能夠理解到這裏既不是 defineProperty 能夠處理的,也不是包一層函數就能解決的,這就是 2.x 版本如今的一個問題。

咱們能夠利用咱們以前的定義來實驗一把:

const vm = new Vue({
  data: {
    userIds: ['01', '02', '03', '04', '05']
  }
})

// 都沒有輸出 視圖更新啦~,說明沒有觸發 set
vm._data.userIds.push('06')  
vm._data.userIds.length = 2
複製代碼

總結

今天關於數據響應式的初探就到這裏吧,說到的東西也挺多的,首先是 MVVM 模式的架構,而後對 MVVM 的每一個組成都進行詳細的說明,接着說到了目前 Vue.js 經過 Object.defineProperty 實現響應式數據的方式,並對 Object.defineProperty 的用法和數據描述符與存取描述符進行了詳細的講解,最後利用 Object.defineProperty 封裝了一個簡單的 Vue 響應式系統,最後的最後提到了關於 Object.defineProperty 的一些缺陷。固然這還只是走出了第一步,Vue.js 的響應式系統還包括數據劫持、依賴收集等,固然後面咱們也會慢慢的提到。

相關文章
相關標籤/搜索