Vue 進階系列之響應式原理及實現

圖片

題圖:本身拍攝於烏鎮閉包

什麼是響應式Reactivity

Reactivity表示一個狀態改變以後,如何動態改變整個系統,在實際項目應用場景中即數據如何動態改變Dom。app

需求

如今有一個需求,有a和b兩個變量,要求b一直是a的10倍,怎麼作?ide

簡單嘗試1:

let a = 3;let b = a * 10;console.log(b); // 30

乍一看好像知足要求,但此時b的值是固定的,無論怎麼修改a,b並不會跟着一塊兒改變。也就是說b並無和a保持數據上的同步。只有在a變化以後從新定義b的值,b纔會變化。函數

a = 4;console.log(a); // 4console.log(b); // 30b = a * 10;console.log(b); // 40

簡單嘗試2:

將a和b的關係定義在函數內,那麼在改變a以後執行這個函數,b的值就會改變。僞代碼以下。this

onAChanged(() => {
    b = a * 10;
})

因此如今的問題就變成了如何實現onAChanged函數,當a改變以後自動執行onAChanged,請看後續。spa

結合view層

如今把a、b和view頁面相結合,此時a對應於數據,b對應於頁面。業務場景很簡單,改變數據a以後就改變頁面b。日誌

<span class="cell b"></span>document
    .querySelector('.cell.b')
    .textContent = state.a * 10

如今創建數據a和頁面b的關係,用函數包裹以後創建如下關係。code

<span class="cell b"></span>onStateChanged(() => {    document
        .querySelector(‘.cell.b’)
        .textContent = state.a * 10})

再次抽象以後以下所示。orm

<span class="cell b">
    {{ state.a * 10 }}
</span>

onStateChanged(() => {
    view = render(state)
})

view = render(state)是全部的頁面渲染的高級抽象。這裏暫不考慮view = render(state)的實現,由於須要涉及到DOM結構及其實現等一系列技術細節。這邊須要的是onStateChanged的實現。對象

實現

實現方式是經過Object.defineProperty中的gettersetter方法。具體使用方法參考以下連接。

MDN之Object.defineProperty

須要注意的是getset函數是存取描述符,valuewritable函數是數據描述符。描述符必須是這兩種形式之一,但兩者不能共存,否則會出現異常。

實例1:實現convert()函數

要求以下:

  • 一、傳入對象obj做爲參數

  • 二、使用Object.defineProperty轉換對象的全部屬性

  • 三、轉換後的對象保留原始行爲,但在get或者set操做中輸出日誌

示例:

const obj = { foo: 123 }
convert(obj)


obj.foo // 輸出 getting key "foo": 123obj.foo = 234 // 輸出 setting key "foo" to 234obj.foo // 輸出 getting key "foo": 234

在瞭解Object.definePropertygettersetter的使用方法以後,經過修改getset函數就能夠實現onAChangedonStateChanged

實現:

function convert (obj) {  // 迭代對象的全部屬性
  // 並使用Object.defineProperty()轉換成getter/setters
  Object.keys(obj).forEach(key => {  
    // 保存原始值
    let internalValue = obj[key]    
    Object.defineProperty(obj, key, {
      get () {        console.log(`getting key "${key}": ${internalValue}`)        return internalValue
      },
      set (newValue) {        console.log(`setting key "${key}" to: ${newValue}`)
        internalValue = newValue
      }
    })
  })
}

實例2:實現Dep

要求以下:

  • 一、建立一個Dep類,包含兩個方法:dependnotify

  • 二、建立一個autorun函數,傳入一個update函數做爲參數

  • 三、在update函數中調用dep.depend(),顯式依賴於Dep實例

  • 四、調用dep.notify()觸發update函數從新運行

示例:

const dep = new Dep()

autorun(() => {
  dep.depend()  console.log('updated')
})// 註冊訂閱者,輸出 updateddep.notify()// 通知改變,輸出 updated

首先須要定義autorun函數,接收update函數做爲參數。由於調用autorun時要在Dep中註冊訂閱者,同時調用dep.notify()時要從新執行update函數,因此Dep中必須持有update引用,這裏使用變量activeUpdate表示包裹update的函數。

實現代碼以下。

let activeUpdate = null function autorun (update) {  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate    // 引用賦值給activeUpdate
    update()                        // 調用update,即調用內部的dep.depend
    activeUpdate = null             // 綁定成功以後清除引用
  }
  wrappedUpdate()                   // 調用}

wrappedUpdate本質是一個閉包,update函數內部能夠獲取到activeUpdate變量,同理dep.depend()內部也能夠獲取到activeUpdate變量,因此Dep的實現就很簡單了。

實現代碼以下。

class Dep {  // 初始化
  constructor () {          
    this.subscribers = new Set()
  }  // 訂閱update函數列表
  depend () {    if (activeUpdate) {     
      this.subscribers.add(activeUpdate)
    }
  }  // 全部update函數從新運行
  notify () {              
    this.subscribers.forEach(sub => sub())
  }
}

結合上面兩部分就是完整實現。

實例3:實現響應式系統

要求以下:

  • 一、結合上述兩個實例,convert()重命名爲觀察者observe()

  • 二、observe()轉換對象的屬性使之響應式,對於每一個轉換後的屬性,它會被分配一個Dep實例,該實例跟蹤訂閱update函數列表,並在調用setter時觸發它們從新運行

  • 三、autorun()接收update函數做爲參數,並在update函數訂閱的屬性發生變化時從新運行。

示例:

const state = {  count: 0}

observe(state)

autorun(() => {  console.log(state.count)
})// 輸出 count is: 0state.count++// 輸出 count is: 1

結合實例1和實例2以後就能夠實現上述要求,observe中修改obj屬性的同時分配Dep的實例,並在get中註冊訂閱者,在set中通知改變。autorun函數保存不變。 實現以下:

class Dep {  // 初始化
  constructor () {          
    this.subscribers = new Set()
  }  // 訂閱update函數列表
  depend () {    if (activeUpdate) {     
      this.subscribers.add(activeUpdate)
    }
  }  // 全部update函數從新運行
  notify () {              
    this.subscribers.forEach(sub => sub())
  }
}function observe (obj) {  // 迭代對象的全部屬性
  // 並使用Object.defineProperty()轉換成getter/setters
  Object.keys(obj).forEach(key => {    let internalValue = obj[key]    // 每一個屬性分配一個Dep實例
    const dep = new Dep()    Object.defineProperty(obj, key, {    
      // getter負責註冊訂閱者
      get () {
        dep.depend()        return internalValue
      },      // setter負責通知改變
      set (newVal) {        const changed = internalValue !== newVal
        internalValue = newVal        
        // 觸發後從新計算
        if (changed) {
          dep.notify()
        }
      }
    })
  })  return obj
}let activeUpdate = nullfunction autorun (update) {  // 包裹update函數到"wrappedUpdate"函數中,
  // "wrappedUpdate"函數執行時註冊和註銷自身
  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate
    update()
    activeUpdate = null
  }
  wrappedUpdate()
}

結合Vue文檔裏的流程圖就更加清晰了。 圖片

Job Done!!!


 圖片

相關文章
相關標籤/搜索