尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
前言
第二屆Vue開發者大會,你有關注嗎?今日早讀文章由汽車之家@花生翻譯投稿分享。
譯者註:本文旨在解釋響應式的原理(也可以說數據雙向綁定的做到),雖然話題較老,但部分觀點很奇特。
@花生,就職於汽車之家用戶產品中心團隊,雲雲搬磚碼農的中的一員。
正文從這開始~~
? 開始
許多前端的Java框架(例如Angular,React和Vue)都有自己的響應式引擎。了解其響應原理及工作原理,有助於提升自己並更加有效地使用框架。在下面的文章中,模擬了Vue源碼中的響應原理,我們一起來看一下。
? 響應式
當你第一次看到Vue工作時,你會覺得它很神奇。以這個簡單的Vue應用程序為例:
<divid=“app”>
<div>Price: ${{ price}}</div>
<div>Total: ${{ price * quantity }}</div>
<div>Taxes: ${{ totalPriceWithTax}}</div>
</div>
<>
varvm =newVue({
el:‘#app’,
data:{
price:5.00,
quantity:2
},
computed:{
totalPriceWithTax(){
returnthis.price *this.quantity *1.03
}
}
})
</>
當price發生變化後,Vue會做三件事:
- 在頁面上更新price的值。
- 重新計算price * quantity的值,並更新頁面。
- 再次調用totalPriceWithTax,並更新頁面。
不過這並不重要,重要的是當price變化時,Vue怎麼知道該更新什麼,以及它是如何跟蹤所有內容的?
通常的Java代碼是做到不了這樣的功能的。那麼我們看下邊的代碼:
let price =5
let quantity =2
let total =price *quantity // => 10
price =20
console.log(`total is ${total}`)
它會列印10:
>>total is10
在Vue中,我們希望price或quantity變化後total跟著更新,我們想要的是如下的結果:
>>total is40
不巧的是,Java不是響應式的,所以我們沒有得到想要的結果。這時候我們就得想點辦法,來達到我們的目的。
⚠️ 問題一
我們需要保存計算total的方法,以便在price或quantity發生變化時再一次調用。
✅ 解決方案
首先,我們需要一些方法來告訴我們的應用程序,「存儲我將要調用的這段代碼,我可能會在其他時間再次調用。」緊接著我們來執行這段代碼,當price或quantity變量更新後,再次運行之前存儲的代碼。
我們可以創建一個記錄函數來保存我們要的東西,這樣我們就可以再次調用它:
let price =5
let quantity =2
let total =0
let target =null
target =()=>{total =price *quantity }
record()
target()
注意,我們在target變量中存儲一個匿名函數,然後調用record函數。record的定義很簡單:
let storge =[]// 用來存儲target
// 記錄函數
functionrecord (){
storge.push(target)
}
我們已經保存了target({total = price * quantity}),因此我們可以之後再運行它,這時我們可以使用一個replay函數,來運行我們所記錄的所有內容。
functionreplay (){
storge.forEach(run =>run())
}
這將遍歷存儲在storage這個數組中的所有匿名函數,並執行每個函數。
然後就會變成這樣:
price =20
console.log(total)// => 10
replay()
console.log(total)// => 40
下面是完整的代碼,你可以通讀以方便理解:
let price =5
let quantity =2
let total =0
let target =null
let storge =[]// 用來存儲target
// 記錄函數
functionrecord (){
storge.push(target)
}
functionreplay (){
storge.forEach(run =>run())
}
target =()=>{total =price *quantity }
record()
target()
price =20
console.log(total)// => 10
replay()
console.log(total)// => 40
⚠️ 問題二
我們可以根據需要,繼續記錄target這類的代碼,但最好有一個一勞永逸的辦法。
✅ 解決方案:依賴類
我們來解決這個問題的方法是將這種行為(target這種匿名函數)封裝到它自己的類中,這是一個標準編程中做到觀察者模式的依賴類。
因此,如果我們創建一個Java類來管理我們的依賴項(使它更接近Vue的處理方式),就像這樣:
classDep{// 例子
constructor (){
this.subscribers =[]// 替代之前的storage
}
depend (){// 替代之前的record
if(target &&!this.subscribers.includes(target)){
this.subscribers.push(target)
}
}
notify (){// 替代之前的replay
this.subscribers.forEach(sub =>sub())// 運行我們的target或觀察者
}
}
注意,我們現在將匿名函數存儲在subscribers中,而不是storage,record也變成了depend,使用notify來代替replay,然後就會變成這樣:
constdep =newDep()
let price =5
let quantity =2
let total =0
let target =()=>{total =price *quantity }
dep.depend()// target添加到subscribers中
target()// 運行並得到total
console.log(total)// => 10
price =20
console.log(total)// => 10
dep.notify()// 調用subscribers里存儲的target
console.log(total)// => 40
改了命名,依舊可以運行,但更適合復用。唯一有點別扭的就是target的存儲和調用。
⚠️ 問題三
我們會為每個變量創建一個依賴類,並且對創建匿名函數的行為進行封裝,從而做到響應式。而不是像這樣調用(這是上面的部分代碼):
target =()=>{total =price *quantity }
dep.depend()
target()
我們可以改為:
watcher(()=>{
total =price *quantity
})
✅ 解決方案: 監聽函數(觀察者模式)
在我們的監聽函數中,我們可以做一些簡單的事情:
functionwatcher(myFun){
target =myFun
dep.depend()
target()
target =null
}
正如你所看到的,watcher函數接受myFunc參數,將其賦給全局的target上,調用dep.depend()將其添加到subscribers里,之後調用並重置target。
運行下面的代碼:
price =20
console.log(total)
dep.notify()
console.log(total)
輸出:
>>10
>>40
還有個問題沒有說,為什麼我們將target設置為全局變量,而不是在需要的時候將其傳遞到函數中。這個答案,請在後邊的內容里尋找。
⚠️ 問題四
我們有一個Dep class,但我們真正想要的是每個變量都有它自己的依賴類,我們把每個屬性都放到一個對象里。
let data ={price:5,quantity:2}
假設一下,我們的每個屬性(price和quantity)都有自己的依賴類。
運行下面的代碼:
watcher(()=>{
total =data.price *data.quantity
})
因為data.price值被訪問,我希望price屬性的依賴類將我們存儲在target中的匿名函數,通過調用dep.depend()將其推到它的訂閱者(用來存儲target)數組中。
同理,因為data.quantity被訪問,我同樣希望quantity屬性的依賴類將這個存儲在target中的匿名函數推入其訂閱者(用來存儲target)數組中。
如果我有另一個匿名函數,里邊只是data.price被訪問,我希望只是將其推送到price屬性的依賴類中。
我們需要在price更新的時候,來調用dep.notify(),我們想要的結果就是這樣的:
console.log(total)// >> 10
price =20// 此時,需要調用price上的notify()
console.log(total)// >> 40
我們需要一些方法來連接data里的屬性(如price或quantity),所以當它被訪問時,我們可以將target保存到我們的訂閱者數組中,當它被改變時,運行我們存儲在訂閱者數組中的函數。
✅ 解決方案: Object.defineProperty()
我們需要了解下ES5中的Object.defineProperty()函數。它可以為屬性定義getter和setter函數。讓我們看一下它的基本用法:
let data ={price:5,quantity:2}
Object.defineProperty(data,‘price’,{
get(){
console.log(`I was accessed`)
},
set(newVal){
console.log(`I was changed`);
}
})
data.price // 調用get() >> I was accessed
data.price =20// 調用set() >> I was changed
如你所見,控制台有兩行輸出,但是,它實際上並沒有get或set任何值,因為我們的用法並不合理。我們現在將其恢復,get()方法返回一個值,set()方法更新一個值,我們添加一個變量internalValue來存儲我們當前的price。
let data ={price:5,quantity:2}
let internalValue =data.price // 初始的值
Object.defineProperty(data,‘price’,{
get(){
console.log(`Gettingprice:${internalValue}`)
returninternalValue
},
set(newVal){
console.log(`Settingprice to:${newVal}`);
internalValue =newVal
}
})
total =data.price *data.quantity // 調用get() >> Getting price: 5
data.price =20// 調用set() >> Setting price to: 20
當我們的get和set正常工作時,控制台輸出的結果也不會出現其他可能。
所以,當我們獲取和設置值時,我們就可以得到我們想要的通知。通過一些遞歸,我們可以為data內的所有屬性運行Object.defineProperty。這時候就可以用到Object.keys(data),像這樣:
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}`)
returninternalValue
},
set(newVal){
console.log(`Setting${key}to:${newVal}`);
internalValue =newVal
}
})
})
total =data.price *data.quantity
data.price =20
現在每個屬性都有了get和set,控制台的輸出很好的證實了這一點。
Gettingprice:5
Gettingquantity:2
Settingprice to:20
? 結合這兩個想法
total =data.price *data.quantity
當這段代碼運行並獲取price值時,我們希望price記住這個匿名函數(target)。這樣,如果price發生變化或者被賦新值時,它就會重新觸發這個函數,因為它知道這一行依賴於它。你可以這樣理解。
Get =>記住這個匿名函數,當我們的值發生變化時,我們會再次運行它。
Set =>運行保存的匿名函數,我們的值發生改變。
或者就我們的Dep Class而言
訪問price (get) => 調用dep.depend()以保存當前target
修改price (set) => 用price調用dep.notify(), 重新運行全部的targets
讓我們結合這兩個想法,然後看看我們的最終代碼:
let data ={price:5,quantity:2}
let target =null
classDep{
constructor (){
this.subscribers =[]
}
depend (){
if(target &&!this.subscribers.includes(target)){
this.subscribers.push(target)
}
}
notify (){
this.subscribers.forEach(sub =>sub())
}
}
Object.keys(data).forEach(key =>{
let internalValue =data[key]
constdep =newDep()
Object.defineProperty(data,key,{
get(){
dep.depend()
returninternalValue
},
set(newVal){
internalValue =newVal
dep.notify()
}
})
})
functionwatcher(myFun){
target =myFun
target()
target =null
}
watcher(()=>{
data.total =data.price *data.quantity
})
現在我們在控制台試一試:
正是我們所希望的!每當price或quantity更新時,我們的total都會賦值。這個來自Vue文檔的插圖的意義就很明顯了。
你看到那個帶getter和setter的data圈(紫色)了嗎?看起來應該很眼熟!每個組件實例都有一個watcher實例(藍色圈),它從getter中收集(紅線)依賴項。稍後調用setter時,它會通知監視器,從而做到重新渲染的功能。下邊是我修改後的插圖:
顯然,Vue在幕後做的更為複雜,但你現在已經對其原理有所了解了。
⏪ 總結
- 如何創建一個Dep class來收集依賴項(depend)並重新運行所有依賴項(notify)。
- 如何創建一個watcher來監聽我們正在運行的代碼,可能需要保存這些代碼(target)並添加為依賴項。
- 怎樣使用Object.defineProperty()創建getter和setter。
關於本文
譯者:@花生
作者:@Gregg Pollack
原文:
https://medium.com/vue-mastery/the-best-explanation-of-java-reactivity-fea6112dd80d
【第1394期】Java 2018 中即將迎來的新功能