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。
<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
中的getter
和setter
方法。具體使用方法參考如下連結。
需要注意的是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文件裡的流程圖就更加清晰了。
Job Done!!!
本文內容參考自VUE作者尤大的付費視訊
交流
本人Github連結如下,歡迎各位Star
我是木易楊,網易高階前端工程師,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!