本文是對於Build a Reactivity System的翻譯前端
使用過vue,而且對於vue實現響應式的原理感興趣的前端童鞋。vue
本教程咱們將使用一些簡單的技術(這些技術你在vue源碼中也能看到)來建立一個簡單的響應式系統。這能夠幫助你更好地理解Vue以及Vue的設計模式,同時可讓你更加熟悉watchers和Dep class.react
當你第一次看到vue的響應式系統工做起來的時候可能會以爲難以想象。es6
舉個例子:npm
<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>
複製代碼
上述例子中,當price改變的時候,vue會作下面三件事情:編程
可是等一下,當price改變的時候vue是怎麼知道要更新哪些東西的?vue是怎麼跟蹤全部東西的?設計模式
若是這對於你來講不是很明顯的話,咱們必須解決的一個大問題是編程一般不會以這種方式工做。舉個例子,若是運行下面的代碼:app
let price = 5
let quantity = 2
let total = price * quantity // 10 right?
price = 20
console.log(`total is ${total}`)
複製代碼
你以爲這段代碼最終打印的結果是多少?由於咱們沒有使用Vue,因此最終打印的值是10ide
>> total is 10
複製代碼
在Vue中咱們想要total的值能夠隨着price或者quantity值的改變而改變,咱們想要:函數
>> total is 40
複製代碼
不幸的是,JavaScript自己是非響應式的。爲了讓total具有響應式,咱們須要使用JavaScript來讓事情表現的有所不一樣。
咱們須要先保存total的計算過程,這樣咱們才能在price或者quantity改變的時候從新執行total的計算過程。
首先,咱們須要告訴咱們的應用,「這段我將要執行的代碼,保存起來,後面我可能須要你再次運行這段代碼。」這樣當price或者quantity改變的時候,咱們能夠再次運行以前保存起來的代碼(來更新total)。
咱們能夠經過將total的計算過程保存成函數的形式來作,這樣後面咱們可以再次執行它。
let price = 5
let quantity = 2
let total = 0
let target = null
target = function () {
total = price * quantity
})
record() // Remember this in case we want to run it later
target() // Also go ahead and run it
複製代碼
請注意咱們須要將匿名函數賦值給target變量,而後調用record函數。使用es6的箭頭函數也能夠寫成下面這樣子:
target = () => { total = price * quantity }
複製代碼
record函數的定義挺簡單的:
let storage = [] // We'll store our target functions in here
function record () { // target = () => { total = price * quantity }
storage.push(target)
}
複製代碼
這裏咱們將target(這裏指的就是:{ total = price * quantity })存起來以便後面能夠運行它,或許咱們能夠弄個replay函數來執行全部存起來的計算過程。
function replay (){
storage.forEach(run => run())
}
複製代碼
replay函數會遍歷全部咱們存儲在storage中的匿名函數而後挨個執行這些匿名函數。 緊接着,咱們能夠這樣用replay函數:
price = 20
console.log(total) // => 10
replay()
console.log(total) // => 40
複製代碼
很簡單吧?如下是完整的代碼。
let price = 5
let quantity = 2
let total = 0
let target = null
let storage = []
function record () {
storage.push(target)
}
function replay () {
storage.forEach(run => run())
}
target = () => { total = price * quantity }
record()
target()
price = 20
console.log(total) // => 10
replay()
console.log(total) // => 40
複製代碼
上面的代碼雖然也能工做,可是可能並很差。或許能夠抽取個class,這個class負責維護targets列表,而後在咱們須要從新運行targets列表的時候接收通知(並執行targets列表中的全部匿名函數)。
一種解決方案是咱們能夠把這種行爲封裝進一個類裏面,一個實現了普通觀察者模式的Dependency Class
因此,若是咱們建立個JavaScript類來管理咱們的依賴的話,代碼可能長成下面這樣:
class Dep { // Stands for dependency
constructor () {
this.subscribers = [] // The targets that are dependent, and should be
// run when notify() is called.
}
depend() { // This replaces our record function
if (target && !this.subscribers.includes(target)) {
// Only if there is a target & it's not already subscribed
this.subscribers.push(target)
}
}
notify() { // Replaces our replay function
this.subscribers.forEach(sub => sub()) // Run our targets, or observers.
}
}
複製代碼
請注意,咱們這裏不用storage,而是用subscribers來存儲匿名函數,同時,咱們不用record而是經過調用depend來收集依賴,而且咱們使用notify替代了原來的replay。如下是Dep類的用法:
const dep = new Dep()
let price = 5
let quantity = 2
let total = 0
let target = () => { total = price * quantity }
dep.depend() // Add this target to our subscribers
target() // Run it to get the total
console.log(total) // => 10 .. The right number
price = 20
console.log(total) // => 10 .. No longer the right number
dep.notify() // Run the subscribers
console.log(total) // => 40 .. Now the right number
複製代碼
上面的代碼和以前的代碼功能上是一致的,可是代碼看起來更具備複用性(Dep類能夠複用)。惟一看起來有點奇怪的地方就是設置和執行target的地方。
後面咱們會爲每一個變量建立個Dep實例,同時若是能夠將建立匿名函數的邏輯封裝起來的話就更好了,或許咱們能夠用個watcher函數來作這件事情。
因此咱們不用經過調用如下代碼來收集依賴
target = () => { total = price * quantity }
dep.depend()
target()
複製代碼
而是經過調用watcher函數來收集依賴(是否是趕腳代碼清晰不少?):
watcher(() => {
total = price * quantity
})
複製代碼
Watcher fucntion的定義以下:
function watcher(myFunc) {
target = myFunc // Set as the active target
dep.depend() // Add the active target as a dependency
target() // Call the target
target = null // Reset the target
}
複製代碼
watcher函數接收一個myFunc,把它賦值給全局的target變量,而後經過調用dep.depend()將target加到subscribers列表中,緊接着調用target函數,而後重置target變量。
如今若是咱們運行如下代碼:
price = 20
console.log(total)
dep.notify()
console.log(total)
複製代碼
>> 10
>> 40
複製代碼
你可能會想爲何要把target做爲一個全局變量,而不是在須要的時候傳入函數。別捉急,這麼作天然有這麼作的道理,看到本教程結尾就闊以一目瞭然啦。
如今咱們有了個簡單的Dep class,可是咱們真正想要的是每一個變量都擁有本身的dep實例,在繼續後面的教程以前讓咱們先把變量變成某個對象的屬性:
let data = { price: 5, quantity: 2 }
複製代碼
讓咱們先假設下data上的每一個屬性(price和quantity)都擁有本身的dep實例。
這樣當咱們運行:
watcher(() => {
total = data.price * data.quantity
})
複製代碼
由於data.price的值被訪問了,我想要price的dep實例能夠將上面的匿名函數收集到本身的subscribers列表裏面。data.quantity也是如此。
若是這時候有個另外的匿名函數裏面用到了data.price,我也想這個匿名函數被加到price自帶的dep類裏面。
問題來了,咱們何時調用price的dep.notify()呢?當price被賦值的時候。在這篇文章的結尾我但願可以直接進入console作如下的事情:
>> total
10
>> price = 20 // When this gets run it will need to call notify() on the price
>> total
40
複製代碼
要實現以上意圖,咱們須要可以在data的全部屬性被訪問或者被賦值的時候執行某些操做。當data下的屬性被訪問的時候咱們就把target加入到subscribers列表裏面,當data下的屬性被從新賦值的時候咱們就觸發notify()執行全部存儲在subscribes列表裏面的匿名函數。
咱們須要學習下Object.defineProperty()函數是怎麼用的。defineProperty函數容許咱們爲屬性定義getter和setter函數,在我使用defineProperty函數以前先舉個很是簡單的例子:
let data = { price: 5, quantity: 2 }
Object.defineProperty(data, 'price', { // For just the price property
get() { // Create a get method
console.log(`I was accessed`)
},
set(newVal) { // Create a set method
console.log(`I was changed`)
}
})
data.price // This calls get()
data.price = 20 // This calls set()
複製代碼
>> I was accessed
>> I was changed
複製代碼
正如你所看到的,上面的代碼僅僅打印兩個log。然而,上面的代碼並不真的get或者set任何值,由於咱們並無實現,下面咱們加上。
let data = { price: 5, quantity: 2 }
let internalValue = data.price // Our initial value.
Object.defineProperty(data, 'price', { // For just the price property
get() { // Create a get method
console.log(`Getting price: ${internalValue}`)
return internalValue
},
set(newVal) { // Create a set method
console.log(`Setting price to: ${newVal}` )
internalValue = newVal
}
})
total = data.price * data.quantity // This calls get()
data.price = 20 // This calls set()
複製代碼
Getting price: 5
Setting price to: 20
複製代碼
因此經過defineProperty函數咱們能夠在get和set值的時候收到通知(就是咱們能夠知道何時屬性被訪問了,何時屬性被賦值了),咱們能夠用Object.keys來遍歷data上全部的屬性而後爲它們添加getter和setter屬性。
let data = { price: 5, quantity: 2 }
Object.keys(data).forEach(key => { // We're running this for each item in data now
let internalValue = data[key]
Object.defineProperty(data, key, {
get() {
console.log(`Getting ${key}: ${internalValue}`)
return internalValue
},
set(newVal) {
console.log(`Setting ${key} to: ${newVal}` )
internalValue = newVal
}
})
})
total = data.price * data.quantity
data.price = 20
複製代碼
如今data上的每一個屬性都有getter和setter了。
total = data.price * data.quantity
複製代碼
當上面的代碼運行而且getprice的值的時候,咱們想要price記住這個匿名函數(target)。這樣當price變更的時候,能夠觸發執行這個匿名函數。
或者:
下面讓咱們把這兩種想法組合起來:
let data = { price: 5, quantity: 2 }
let target = null
// This is exactly the same Dep class
class Dep {
constructor () {
this.subscribers = []
}
depend() {
if (target && !this.subscribers.includes(target)) {
// Only if there is a target & it's not already subscribed
this.subscribers.push(target)
}
}
notify() {
this.subscribers.forEach(sub => sub())
}
}
// Go through each of our data properties
Object.keys(data).forEach(key => {
let internalValue = data[key]
// Each property gets a dependency instance
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend() // <-- Remember the target we're running
return internalValue
},
set(newVal) {
internalValue = newVal
dep.notify() // <-- Re-run stored functions
}
})
})
// My watcher no longer calls dep.depend,
// since that gets called from inside our get method.
function watcher(myFunc) {
target = myFunc
target()
target = null
}
watcher(() => {
data.total = data.price * data.quantity
})
複製代碼
在控制檯看下:
下面的圖如今應該看起來有點感受了: