深刻響應式原理 — Vue.js
https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d
https://www.vuemastery.com/courses/advanced-components/evan-you-on-proxies/javascript
不少前端 JavaScript 框架,包含但不限於(Angular,React,Vue)都擁有本身的響應式引擎。經過了解響應式變成原理以及具體的實現方式,能夠提成對既有響應式框架的高效應用。html
咱們看一下 Vue 的響應式系統:前端
<div id="app">
<div>Price: ${{ price }}</div>
<div>Total: ${{ price * quantity }}</div>
<div>Taxes: ${{ totalPriceWithTax }}</div>
<div>
複製代碼
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
price: 5.00,
quantity: 2
},
computed: {
totalPriceWithTax() {
return this.price * this.quantity * 1.03
}
}
})
</script>
複製代碼
Vue 知道每當 price
發生變換時,它作了以下三件事情:vue
price
的值。price * quantity
表達式,並更新。totalPriceWithTax
方法,並更新。可是等一下,這裏有些疑惑,Vue 是怎麼知道 price
更新了呢,如何去追蹤更新的具體過程呢?java
let price = 5
let quantity = 2
let total = price * quantity // 10 ?
price = 20
console.log(`total is ${total}`) // 10
// 咱們想要的 total 值但願是更新後的 40
複製代碼
咱們須要將 total
的計算過程存起來,這樣咱們就可以在 price
或者 quantity
變化時運行計算過程。react
首先,咱們須要告訴應用程序,「這裏有一個關於計算的方法,存起來,我會在數據更新的時候去運行它。「npm
咱們建立一個記錄函數並運行它:數組
let price = 5
let quantity = 2
let total = 0
let target = null
target = () => { total = price * quantity }
record() // 記錄咱們想要運行的實例
target() // 運行 total 計算過程
複製代碼
簡單定義一個 record
:bash
let storage = [] // 將 target 函數存在這裏
function record () { // target = () => { total = price * quantity }
storage.push(target)
}
複製代碼
咱們存儲了 target
函數,咱們須要運行它,須要頂一個 replay
函數來運行咱們記錄的函數:app
function replay () {
storage.forEach( run => run() )
}
複製代碼
在代碼中咱們運行:
price = 20
console.log(total) // => 10
replay()
console.log(total) // => 40
複製代碼
是否是足夠的簡單,代碼可讀性好,而且能夠運行屢次。FYI,這裏用了最基礎的方式進行編碼,先掃盲。
let price = 5
let quantity = 2
let total = 0
let target = null
let storage = []
target = () => { total = price * quantity }
function record () {
storage.push(target)
}
function replay () {
storage.forEach( run => run() )
}
record()
target()
price = 20
console.log(total) // => 10
replay()
console.log(total) // => 40
複製代碼
咱們能夠不斷記錄咱們須要的 target
, 並進行 record
,可是咱們須要更健壯的模式去擴展咱們的應用。也許面向對象的方式能夠維護一個 targe 列表,咱們用通知的方式進行回調。
咱們經過一個屬於本身的類進行行爲的封裝,一個標準的依賴類 Dependency Class,實現觀察者模式。
若是咱們想要建立一個管理依賴的類,標準模式以下:
class Dep { // Stands for dependency
constructor () {
this.subscribers = [] // 依賴數組,當 notify() 調用時運行
}
depend () {
if (target && !this.subscribers.includes(target)) {
// target 存在而且不存在於依賴數組中,進行依賴注入
this.subscribers.push(target)
}
}
notify () { // 替代以前的 replay 函數
this.subscribers.forEach(sub => sub()) // 運行咱們的 targets,或者觀察者函數
}
}
複製代碼
注意以前替換的方法,storage
替換成了構造函數中的 subscribers
。recod
函數替換爲 depend
。replay
函數替換爲 notify
。
如今在運行:
const dep = new Dep()
let price = 5
let quantity = 2
let total = 0
let target = () => { total = price * quantity }
dep.depend() // 依賴注入
target() // 計算 total
console.log(total) // => 10
price = 20
console.log(total) // => 10
dep.notify()
console.log(total) // => 40
複製代碼
工做正常,到這一步感受奇怪的地方,配置 和 運行 target
的地方。
以後咱們但願可以將 Dep 類應用在每個變量中,而後優雅地經過匿名函數去觀察更新。可能須要一個觀察者 watcher
函數去知足這樣的行爲。
咱們須要替換的代碼:
target = () => { total = price * quantity }
dep.depend()
target()
複製代碼
替換爲:
watcher( () => {
total = price * quantit
})
複製代碼
咱們先定義一個簡單的 watcher 函數:
function watcher (myFunc) {
target = myFunc // 動態配置 target
dep.depend() // 依賴注入
target() // 回調 target 方法
target = null // 重置 target
}
複製代碼
正如你所見, watcher
函數傳入一個 myFunc
的形參,配置全局變量 target
,調用 dep.depend()
進行依賴注入,回調 target
方法,最後,重置 target
。
運行一下:
price = 20
console.log(total) // => 10
dep.notify()
console.log(total) // => 40
複製代碼
你可能會質疑,爲何咱們要對一個全局變量的 target
進行操做,這顯得很傻,爲何不用參數傳遞進行操做呢?文章的最後將揭曉答案,答案也是顯而易見的。
咱們如今有一個單一的 Dep class
,可是咱們真正想要的是咱們每個變量都擁有本身的 Dep
。讓咱們先將數據抽象到 properties
。
let data = { price: 5, quantity: 2 }
複製代碼
將設每一個屬性都有本身的內置 Dep 類
然我咱們運行:
watcher( () => {
total = data.price * data.quantit
})
複製代碼
當 data.price
的 value 開始存取時,我想讓關於 price
屬性的 Dep 類 push 咱們的匿名函數(存儲在 target 中)進入 subscriber 數組(經過調用 dep.depend())。而當 quantity
的 value 開始存取時,咱們也作一樣的事情。
若是咱們有其餘的匿名函數,假設存取了 data.price
,一樣的在 price
的 Dep 類中 push 此匿名函數。
當咱們想經過 dep.notify()
進行 price
的依賴回調時候。咱們想 在 price
set 時候讓回調執行。在最後咱們要達到的效果是:
$ total
10
$ price = 20 // 回調 notify() 函數
$ total
40
複製代碼
咱們須要學習一下關於 Object.defineProperty() - JavaScript | MDN。它容許咱們在 property 上定義 getter 和 setter 函數。咱們展現一下最基本的用法:
let data = { price: 5, quantity: 2 }
Object.defineProperty(data, 'price', { // 僅定義 price 屬性
get () { // 建立一個 get 方法
console.log(`I was accessed`)
},
set (newVal) { // 建立一個 set 方法
console.log(`I was changed`)
}
})
data.price // 回調 get()
// => I was accessed
data.price = 20 // 回調 set()
// => I was changed
複製代碼
正如你所見,打印兩行 log。然而,這並不能推翻既有功能, get
或者 set
任意的 value
。get()
指望返回一個 value,set()
須要持續更新一個值,因此咱們加入 internalValue
變量用於存儲 price
的值。
let data = { price: 5, quantity: 2 }
let internalValue = data.price // 初始值
Object.defineProperty(data, 'price', { // 僅定義 price 屬性
get () { // 建立一個 get 方法
console.log(`Getting price: ${internalValue}`)
return internalValue
},
set (newVal) { // 建立一個 set 方法
console.log(`Setting price: ${newVal}`)
internalValue = newVal
}
})
total = data.price * data.quantity // 回調 get()
// => Getting price: 5
data.price = 20 // 回調 set()
// => Setting price: 20
複製代碼
至此,咱們有了一個當 get 或者 set 值的時候的通知方法。咱們也能夠用某種遞歸能夠將此運行在咱們的數據隊列中?
FYI,Object.keys(data)
返回對象的 key 值列表。
let data = { price: 5, quantity: 2 }
Object.keys(data).forEach(key => {
let internalValue = data[key]
Object.defineProperty(data, key, {
get () {
console.log(`Getting ${key}: ${internalValue}`)
return internalValue
},
set (newVal) {
console.log(`Setting ${key}: ${newVal}`)
internalValue = newVal
}
})
})
total = data.price * data.quantity
// => Getting price: 5
// => Getting quantity: 2
data.price = 20
// => Setting price: 20
複製代碼
將全部的理念集成起來
total = data.price * data.quantity
複製代碼
當代碼碎片好比 get 函數的運行而且 get 到 price
的值,咱們須要 price
記錄在匿名函數 function(target) 中,若是 price
變化了,或者 set 了一個新的 value,會觸發這個匿名函數而且 get return,它可以知道這裏更新了同樣。因此咱們能夠作以下抽象:
Get => 記錄這個匿名函數,若是值更新了,會運行此匿名函數
Set => 運行保存的匿名函數,僅僅改變保存的值。
在咱們的 Dep 類的實例中,抽象以下:
Price accessed (get) => 回調 dep.depend()
去注入當前的 target
Price set => 回調 price 綁定的 dep.notify()
,從新計算全部的 targets
讓咱們合併這兩個理念,生成最終代碼:
let data = { price: 5, quantity: 2 }
let target = null
class Dep { // Stands for dependency
constructor () {
this.subscribers = [] // 依賴數組,當 notify() 調用時運行
}
depend () {
if (target && !this.subscribers.includes(target)) {
// target 存在而且不存在於依賴數組中,進行依賴注入
this.subscribers.push(target)
}
}
notify () { // 替代以前的 replay 函數
this.subscribers.forEach(sub => sub()) // 運行咱們的 targets,或者觀察者函數
}
}
// 遍歷數據的屬性
Object.keys(data).forEach(key => {
let internalValue = data[key]
// 每一個屬性都有一個依賴類的實例
const dep = new Dep()
Object.defineProperty(data, key, {
get () {
dep.depend()
return internalValue
},
set (newVal) {
internalValue = newVal
dep.notify()
}
})
})
// watcher 再也不調用 dep.depend
// 在數據 get 方法中運行
function watcher (myFunc) {
target = myFunce
target()
target = null
}
watcher(()=> {
data.total = data.price * data.quantity
})
data.total
// => 10
data.price = 20
// => 20
data.total
// => 40
data.quantity = 3
// => 3
data.total
// => 60
複製代碼
這已經達到了咱們的指望,price
和 quantity
都成爲響應式的數據了。
Vue 的數據響應圖以下:
看到紫色的 Data 數據,裏面的 getter 和 setter?是否是很熟悉了。每一個組件的實例會有一個 watcher
的實例(藍色圓圈), 從 getter 中收集依賴。而後 setter 會被回調,這裏 notifies 通知 watcher 讓組件從新渲染。下圖是本例抽象的狀況:
顯然,Vue 的內部轉換相對於本例更復雜,但咱們已經知道最基本的了。