--圖片來源vue2.6正式版本(代號:超時空要塞)釋出時,尤雨溪推送配圖。
前言
其實這個冷飯我並不想炒,畢竟vue3馬上都要出來。我還在這裡炒冷飯,那明顯就是搞事情。
起因:
作為切圖仔搬磚汪,長期切圖jq一把梭。重複繁瑣的切圖,讓自己陷入了一個無限的圍城。想出去切圖這個圍城看一看,但是又害怕因為切圖時間久了,自己會的也只有切圖了。
為了後面能夠繼續搬磚恰飯,幫助自己跳出切圖仔的圍城。也去看了vue相關文件,當時記憶深刻覺得還行。可是G胖這個時候發動小紫本和打折魔咒,不知不覺又沉迷於DOTA小本子上面了。關於vue響應式原理很快忘得一塌糊塗,只記得一個屬性Object.defindProperty
,然後就沒有然後了......
為了避免自己後面再次忘記,所以這裡炒一個冷飯加深記憶。
響應式vue
在講解vue響應式的原理之前,讓我們來一段Vue程式碼作為示例:
<div id="app">
<div>主食: {{ food }}</div>
<div>飲料: {{ drink }}</div>
<div>選單: {{ menu }}</div>
</div>
<script>
let vue = new Vue({
el: '#app',
data: {
food: '煎餅果子',
drink: '熱豆漿'
},
computed: {
menu() {
return this.food + this.drink
}
}
})
</script>
當food
和drink
發生變化後,Vue會做兩件事:
在頁面上更新
food
和drink
的值。再次呼叫
menu
, 重新計算food + drink
的值, 並在頁面上面更新。
更新值+計算值做的事情其實很簡單,幾行程式碼的事情。問題是當food
或者drink
變化時,Vue是怎麼知道誰變化,然後馬上響應其行為,去執行那"簡單的幾行程式碼"?
所以,當看到Vue案例時,詞窮的我當時第一反應就是牛皮
。
之所以發出感嘆,是因為通常的JavaScript程式碼是實現不了這樣的功能的。話不多說,讓我們直接上程式碼來說明:
let food = "煎餅果子"
let drink = "熱豆漿"
let menu = null
menu = food + drink
food = '炸雞漢堡'
drink = '快樂水'
console.log(menu)
最終控制檯列印結果:
煎餅果子熱豆漿
如果是在Vue當中,food
和drink
發生了變化,那麼Vue會跟著做出響應動作,從而在控制檯輸出我們想要的結果:
炸雞漢堡快樂水
選單響應
這裡就出現第一個問題,當food
或者drink
發生變化之後,menu
並不會響應其變化。這個時候就需要我們來解決這個問題,滿足menu
響應。
借鑑Vue一樣,我們先把menu
的計算方法。也寫成一個函式,取名為target
。然後每次food
或者drink
變化的時候呼叫target
函式
let food = "煎餅果子"
let drink = "熱豆漿"
let menu = null
let target = () => {
menu = food + drink
}
target() // 初始化選單menu
food = '炸雞漢堡'
drink = '快樂水'
target()
console.log(menu)
控制檯輸出:
炸雞漢堡快樂水
浴室沉思
前面一把梭直接呼叫的滿足menu
響應的問題,但是也間接留下一個新的疑惑點。這裡針對一個選單,就寫了一個target。假設有多個選單需要響應呢?
例如:
單人早餐 = 煎餅果子 + 熱豆漿
豪華套餐: 煎餅果子加兩雞蛋 + 熱豆漿 + 油條一根午餐
- ......
如果這個時候切換成:
單人午餐 = 炸雞漢堡 + 快樂水
豪華套餐: 雙層炸雞漢堡 + 快樂水 + 快樂薯條一包
- ......
按照前面的邏輯, 估計得寫N個target。這個時候響應式又是一個麻煩事情,可是有句話說的好。梭哈一時爽,一直梭哈一直爽。
既然前面直接採用target一把梭完成,所以針對N個target方法,我也可以直接來個for迴圈一把梭能完成響應式問題。
for迴圈一把梭
- 定義一個陣列,每定義了一個target函式。就儲存到陣列當中。
let storge = [] // 用來儲存target
function record (){ //
storge.push(target)
}
- 定義迴圈函式,每次
data
有變更。就呼叫這個函式,進行一把for
迴圈.
function replay (){
storge.forEach(run => run())
}
- 合併成完整的程式碼:
let food = "煎餅果子"
let drink = "熱豆漿"
let menu = null
food = '炸雞漢堡'
drink = '快樂水'
let target = () => {
menu = food + drink
}
let storge = []; //用來儲存更多的target
function record(target) {
storge.push(target)
}
function replay() {
storge.forEach(run => run())
}
record(target)
replay()
food = '炸雞漢堡'
drink = '快樂水'
replay()
console.log(menu)
最後控制檯成功輸出:
炸雞漢堡快樂水
Dep依賴類
通過一把梭實現功能,那麼接下來就開始思考優化部分了。繼續記錄target這類的程式碼,這樣有點怪怪的。為了後面方便管理,我們把程式碼進行簡單的優化,封裝成一個類:
class Dep {
constructor() {
this.subs = []
}
// 收集依賴
depend(sub) {
if (sub && !this.subs.includes(sub)) { // 做一個判斷
this.subs.push(sub)
}
}
notify() {
console.log("暗號:下雨啦,收衣服啦!")
this.subs.forEach(sub => sub()) // 執行我們的target
}
}
就這樣target
函式儲存在類的subs
中,record
也變成了depend
,使用notify
來代替replay
封裝成類之後,每次當data資料更新的時候,就會發出一個暗號下雨啦,收衣服啦!
然後就開始遍歷執行相應的target
依賴了。
新的呼叫程式碼就更加清晰明瞭:
let dep = new Dep()
let food = "煎餅果子"
let drink = "熱豆漿"
let menu = null
let target = () => {
menu = food + drink
}
dep.depend(target)
target() // 完成menu第一次初始化
console.log(menu)
food = '炸雞漢堡'
drink = '快樂水'
dep.notify()
console.log(menu)
控制檯輸出:
煎餅果子熱豆漿
暗號:'下雨啦,收衣服啦!'
炸雞漢堡快樂水
觀察者亮相
當前的程式碼,是確定一個依賴事件,就定義target
,然後呼叫依賴類dep.depend
將其儲存起來。
let target = () => { menu = food + drink }
dep.depend(target)
target()
這個時候又新來一個target
事件又該如何做:
新新增一個target
事件?
let target2 = () => { 新的依賴事件 }
dep.depend(target2)
target2()
要是有幾百個依賴,那還不得上天。我估計要是這樣寫程式碼,估計你的同事要說你寫程式碼像CXK
觀察者函式
借鑑觀察者模式,封裝一個watcher
函式. 幫你觀察記錄相關target
事件,避免多次宣告變數。
function watcher(myFun) {
target = myFun
dep.depend(target)
target()
target = null
}
watcher(() => {
menu = food + drink
})
正如你所看到的,watcher
函式接受myFunc
引數,將其賦給全域性的target
上,呼叫dep.depend()
將其新增到陣列裡,之後呼叫並重置target
。
既然又封裝一個新的函式,那麼驗證又將是必不可少的了。這裡我們修改一下drink
來試試:
drink = "快樂水"
console.log(menu)
dep.notify()
console.log(menu)
控制檯輸出結果:
煎餅果子熱豆漿
暗號:下雨啦,收衣服啦!
煎餅果子快樂水
Object.defineProperty()
基本用法
鋪墊了這麼久,一個關鍵性角色這個時候也登場了。
該方法允許精確新增或修改物件的屬性。通過賦值操作新增的普通屬性是可列舉的,能夠在屬性列舉期間呈現出來(for...in 或 Object.keys 方法), 這些屬性的值可以被改變,也可以被刪除。這個方法允許修改預設的額外選項(或配置)。預設情況下,使用 Object.defineProperty() 新增的屬性值是不可修改的。
--《MDN文件》
不明覺厲? 那就先熱身一下,進入快樂的舉例子環節:
let data = {
food: '煎餅果子',
drink: '熱豆漿'
}
Object.defineProperty(data, 'food', {
get() {
console.log(`觸發get方法`)
},
set(newVal) {
console.log(`設定food為${newVal}`)
}
})
data.food
data.food = 炸雞漢堡
控制檯輸出:
觸發get方法
設定food為炸雞漢堡
簡單封裝
但是僅僅憑藉object.defineProperty
是無法完成當一個資料更新了,完成資料響應。而且程式碼這裡也是隻是對food
做了一個處理, 還有drink
沒有處理,所以為了完成data所以屬性都做相應的處理。接下來就是對於Object.defineProperty()
進行簡單的封裝處理了:
Object.keys(data).forEach(key => {
let value = data[key]
Object.defineProperty(data, key, {
get() {
return value
},
set(newVal) {
value = newVal
}
})
})
遍歷了data每個屬性,然後對每個屬性進行偵聽。這樣data的屬性一旦改變,就會自動發出通知.
程式碼整合
前面零零散散分別講了 Dep
、watcher
和object.defineProperty
, 那麼接下來就讓我們把這個幾個部分整合到一起,完整檢視整個程式碼:
let data = {
food: '煎餅果子',
drink: '熱豆漿'
}
class Dep {
constructor() {
this.subs = []
}
// 收集依賴
depend(sub) {
if (sub && !this.subs.includes(sub)) { // 做一個判斷
this.subs.push(sub)
}
}
notify() {
console.log("暗號:下雨啦,收衣服啦!")
this.subs.forEach(sub => sub()) // 執行我們的target
}
}
Object.keys(data).forEach(key => {
let value = data[key]
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend(target)
return value
},
set(newVal) {
value = newVal
dep.notify()
}
})
})
function watcher(myFun) {
target = myFun
// dep.depend(target) 這裡修改,移動到Object.defineProperty當中去
target()
target = null
}
watcher(() => {
data.menu = data.food + data.drink
})
console.log(data.menu)
data.food = "炸雞漢堡"
data.drink = "快樂水"
console.log(data.menu)
控制檯輸出:
煎餅果子熱豆漿
暗號:下雨啦,收衣服啦!
炸雞漢堡快樂
這裡完全實現了文章開頭所提出的需求,每當food
或drink
更新時,我們的menu
也會跟著響應並更新。
這時候Vue文件的插圖的意義就很明顯了:
免責宣告
以上就是我的炒冷飯內容,怕忘記重寫總結一下,有說錯的地方多擔待。(特拿前端勸退師騷宣告一份,窺伺好久了。)
意思就是寫得略粗糙,別噴我。。。
我是車大棒,我為我自己插眼。