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

什麼是響應式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。


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

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


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

再次抽象之後如下所示。


{{ state.a * 10 }}

onStateChanged(() => {
view = render(state)
})
複製代碼

view = render(state)是所有的頁面渲染的高級抽象。這裡暫不考慮view = render(state)的實現,因為需要涉及到DOM結構及其實現等一系列技術細節。這邊需要的是onStateChanged的實現。

實現

實現方式是通過Object.defineProperty中的getter和setter方法。具體使用方法參考如下鏈接。

MDN之Object.defineProperty

需要注意的是get和set函數是存取描述符,value和writable函數是數據描述符。描述符必須是這兩種形式之一,但二者不能共存,不然會出現異常。

實例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.defineProperty中getter和setter的使用方法之後,通過修改get和set函數就可以實現onAChanged和onStateChanged。

實現:

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類,包含兩個方法:depend和notify
  • 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文檔裡的流程圖就更加清晰了。

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

Job Done!!!

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

鏈接:https://juejin.im/post/5bce6a26e51d4579e9711f1d


分享到:


相關文章: