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

木易楊說發表於2018-10-23

Vue進階系列彙總如下,歡迎閱讀。

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

Vue 進階系列(二)之外掛原理及實現

Vue 進階系列(三)之Render函式原理及實現

什麼是響應式Reactivity

Reactivity表示一個狀態改變之後,如何動態改變整個系統,在實際專案應用場景中即資料如何動態改變Dom。

需求

現在有一個需求,有a和b兩個變數,要求b一直是a的10倍,怎麼做?

簡單嘗試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); // 4
console.log(b); // 30
b = a * 10;
console.log(b); // 40
複製程式碼

簡單嘗試2:

將a和b的關係定義在函式內,那麼在改變a之後執行這個函式,b的值就會改變。虛擬碼如下。

onAChanged(() => {
    b = a * 10;
})
複製程式碼

所以現在的問題就變成了如何實現onAChanged函式,當a改變之後自動執行onAChanged,請看後續。

結合view層

現在把a、b和view頁面相結合,此時a對應於資料,b對應於頁面。業務場景很簡單,改變資料a之後就改變頁面b。

<span class="cell b"></span>

document
    .querySelector('.cell.b')
    .textContent = state.a * 10
複製程式碼

現在建立資料a和頁面b的關係,用函式包裹之後建立以下關係。

<span class="cell b"></span>

onStateChanged(() => {
    document
        .querySelector(‘.cell.b’)
        .textContent = state.a * 10
})
複製程式碼

再次抽象之後如下所示。

<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()函式

要求如下:

  • 1、傳入物件obj作為引數
  • 2、使用Object.defineProperty轉換物件的所有屬性
  • 3、轉換後的物件保留原始行為,但在get或者set操作中輸出日誌

示例:

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


obj.foo // 輸出 getting key "foo": 123
obj.foo = 234 // 輸出 setting key "foo" to 234
obj.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

要求如下:

  • 1、建立一個Dep類,包含兩個方法:dependnotify
  • 2、建立一個autorun函式,傳入一個update函式作為引數
  • 3、在update函式中呼叫dep.depend(),顯式依賴於Dep例項
  • 4、呼叫dep.notify()觸發update函式重新執行

示例:

const dep = new Dep()

autorun(() => {
  dep.depend()
  console.log('updated')
})
// 註冊訂閱者,輸出 updated

dep.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:實現響應式系統

要求如下:

  • 1、結合上述兩個例項,convert()重新命名為觀察者observe()
  • 2、observe()轉換物件的屬性使之響應式,對於每個轉換後的屬性,它會被分配一個Dep例項,該例項跟蹤訂閱update函式列表,並在呼叫setter時觸發它們重新執行
  • 3、autorun()接收update函式作為引數,並在update函式訂閱的屬性發生變化時重新執行。

示例:

const state = {
  count: 0
}

observe(state)

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

state.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 = null

function autorun (update) {

  // 包裹update函式到"wrappedUpdate"函式中,
  // "wrappedUpdate"函式執行時註冊和登出自身
  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate
    update()
    activeUpdate = null
  }
  wrappedUpdate()
}
複製程式碼

結合Vue文件裡的流程圖就更加清晰了。

image

Job Done!!!

本文內容參考自VUE作者尤大的付費視訊

交流

本人Github連結如下,歡迎各位Star

github.com/yygmind/blo…

我是木易楊,網易高階前端工程師,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!

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

相關文章