溫故而知新,同樣的知識(shí)點(diǎn)族操,再次學(xué)習(xí)的時(shí)候也可以有新的認(rèn)識(shí),這里把 reactive 和相關(guān)函數(shù)總體介紹一下。
從 ES6 的 Proxy 開(kāi)始色难,到Vue3提供的各種函數(shù)泼舱,盡量的把自己的理解融入進(jìn)來(lái),當(dāng)然目前水平限制枷莉,無(wú)法做到非常深入的程度娇昙。以后有更深的了解之后還會(huì)有新的見(jiàn)解。
ES6的Proxy
Proxy 是 ES6 提供的一個(gè)可以攔截對(duì)象基礎(chǔ)操作的代理笤妙。因?yàn)?reactive 采用 Proxy 代理的方式冒掌,實(shí)現(xiàn)引用類(lèi)型的響應(yīng)性,所以我們先看看 Proxy 的基礎(chǔ)使用方法蹲盘,以便于我理解 reactive 的結(jié)構(gòu)股毫。
我們先來(lái)定義一個(gè)函數(shù),了解一下 Proxy 的基本使用方式:
// 定義一個(gè)函數(shù)召衔,傳入對(duì)象原型铃诬,然后創(chuàng)建一個(gè)Proxy的代理
const myProxy = (_target) => {
// 定義一個(gè) Proxy 的實(shí)例
const proxy = new Proxy(_target, {
// 攔截 get 操作
get: function (target, key, receiver) {
console.log(`getting ${key}!`, target[key])
// 用 Reflect 調(diào)用原型方法
return Reflect.get(target, key, receiver)
},
// 攔截 set 操作
set: function (target, key, value, receiver) {
console.log(`setting ${key}:${value}!`)
// 用 Reflect 調(diào)用原型方法
return Reflect.set(target, key, value, receiver)
}
})
// 返回實(shí)例
return proxy
}
// 使用方法,是不是和reactive有點(diǎn)像苍凛?
const testProxy = myProxy({
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
console.log('自己定義的Proxy實(shí)例:')
console.log(testProxy)
// 測(cè)試攔截情況
testProxy.name = '新的名字' // set操作
console.log(testProxy.name) // get 操作
Proxy 有兩個(gè)參數(shù) target 和 handle氧急。
- target:要代理的對(duì)象,也可以是數(shù)組毫深,但是不能是基礎(chǔ)類(lèi)型吩坝。
- handler:設(shè)置要攔截的操作,這里攔截了 set 和 get 操作哑蔫,當(dāng)然還可以攔截其他操作钉寝。
我們先來(lái)看一下運(yùn)行結(jié)果:
- Handler 可以看到我們寫(xiě)的攔截函數(shù) get 和 set;
- Target 可以看到對(duì)象原型闸迷。
注意:這里只是實(shí)現(xiàn)了 get 和 set 的攔截嵌纲,并沒(méi)有實(shí)現(xiàn)數(shù)據(jù)的雙向綁定,模板也不會(huì)自動(dòng)更新內(nèi)容腥沽,Vue內(nèi)部做了很多操作才實(shí)現(xiàn)了模板的自動(dòng)更新功能逮走。
用 Proxy 給 reactive 套個(gè)娃,會(huì)怎么樣今阳?
有個(gè)奇怪的地方师溅,既然 Proxy 可以實(shí)現(xiàn)對(duì) set 等操作的攔截,那么 reactive 為啥不返回一個(gè)可以監(jiān)聽(tīng)的鉤子呢盾舌?為啥要用 watch 來(lái)實(shí)現(xiàn)監(jiān)聽(tīng)的工作墓臭?
為啥會(huì)這么想?因?yàn)榭吹搅?Vuex4.0 的設(shè)計(jì)妖谴,明明已經(jīng)把 state 整體自動(dòng)變成了 reactive 的形式窿锉,那么為啥還非得在 mutations 里寫(xiě)函數(shù),實(shí)現(xiàn) set 操作呢?好麻煩的樣子嗡载。
外部直接對(duì) reactive 進(jìn)行操作窑多,然后 Vuex 內(nèi)部監(jiān)聽(tīng)一下,這樣大家不就都省事了嗎洼滚?要實(shí)現(xiàn)插件功能埂息,還是跟蹤功能,不都是可以自動(dòng)實(shí)現(xiàn)了嘛判沟。
所以我覺(jué)得還是可以套個(gè)娃的耿芹。
實(shí)現(xiàn)模板的自動(dòng)刷新
本來(lái)以為上面那個(gè) myProxy 函數(shù),傳入一個(gè) reactive 之后挪哄,就可以自動(dòng)實(shí)現(xiàn)更新模板的功能了吧秕,結(jié)果模板沒(méi)理我。
這不對(duì)呀迹炼,我只是監(jiān)聽(tīng)了一下砸彬,不是又交給 reactive 了嗎?為啥模板不理我斯入?
經(jīng)過(guò)各種折騰砂碉,終于找到了原因,于是函數(shù)改成了這樣:
/**
* 用 Proxy定義一個(gè) reactive 的套娃刻两,實(shí)現(xiàn)可以監(jiān)聽(tīng)任意屬性變化的目的增蹭。(不包含嵌套對(duì)象的屬性)
* @param {*} _target 要攔截的目標(biāo)
* @param {*} callback 屬性變化后的回調(diào)函數(shù)
*/
const myReactive = (_target, callback) => {
let _change = (key, value) => {console.log('內(nèi)部函數(shù)')}
const proxy = new Proxy(_target, {
get: function (target, key, receiver) {
if (typeof key !== 'symbol') {
console.log(`getting ${key}!`, target[key])
} else {
console.log('getting symbol:', key, target[key])
}
// 調(diào)用原型方法
return Reflect.get(target, key, receiver)
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}:${value}!`)
// 源頭監(jiān)聽(tīng)
if (typeof callback === 'function') {
callback(key, value)
}
// 任意位置監(jiān)聽(tīng)
if (typeof _target.__watch === 'function') {
_change(key, value)
}
// 調(diào)用原型方法
return Reflect.set(target, key, value, target) // 這里有變化,最后一個(gè)參數(shù)改成 target
}
})
// 實(shí)現(xiàn)任意位置的監(jiān)聽(tīng)磅摹,
proxy.__watch = (callback) => {
if (typeof callback === 'function') {
_change = callback
}
}
// 返回實(shí)例
return proxy
}
代碼稍微多了一些滋迈,我們一塊一塊看。
get
這里要做一下 symbol 的判斷户誓,否則會(huì)報(bào)錯(cuò)饼灿。好吧,其實(shí)我們似乎不需要 console.log帝美。set
這里改了一下最后一個(gè)參數(shù)碍彭,這樣模板就可以自己更新了。設(shè)置 callback 函數(shù)悼潭,實(shí)現(xiàn)源頭監(jiān)聽(tīng)
設(shè)置一個(gè)回調(diào)函數(shù)庇忌,才能在攔截到set操作的時(shí)候,通知外部的調(diào)用者女责。只是這樣只適合于定義實(shí)例的地方漆枚。那么接收參數(shù)的地方怎么辦呢?
調(diào)用方法如下:
// 定義一個(gè)攔截reactive的Proxy
// 并且實(shí)現(xiàn)源頭的監(jiān)聽(tīng)
const myProxyReactive = myReactive(retObject,
((key, value) =>{
console.log(`ret外部獲得通知:${key}:${value}`)
})
)
這樣我們就可以在回調(diào)函數(shù)里面得到修改的屬性名稱抵知,以及屬性值。
這樣我們做狀態(tài)管理的時(shí)候,是不是就不用特意去寫(xiě) mutations 里面的函數(shù)了呢刷喜?
- 內(nèi)部設(shè)置一個(gè)鉤子函數(shù)
設(shè)置一個(gè) _change() 鉤子函數(shù)残制,這樣接收參數(shù)的地方,可以通過(guò)這個(gè)鉤子來(lái)得到變化的通知掖疮。
調(diào)用方法如下:
// 任意位置的監(jiān)聽(tīng)
myProxyReactive.__watch((key, value) => {
console.log(`任意位置的監(jiān)聽(tīng):${key}:${value}`)
})
只是好像哪里不對(duì)的樣子初茶。
首先這個(gè)鉤子沒(méi)找到合適的地方放,目前放在了原型對(duì)象上面浊闪,就是說(shuō)破壞了原型對(duì)象的結(jié)構(gòu)恼布,這個(gè)似乎會(huì)有些影響。
然后搁宾,接收參數(shù)的地方折汞,不是可以直接得到修改的情況嗎?是否還需要做這樣的監(jiān)聽(tīng)盖腿?
最后爽待,好像沒(méi)有 watch 的 deep 監(jiān)聽(tīng)來(lái)的方便,那么問(wèn)題又來(lái)了翩腐,為啥 Vuex 不用 watch 呢鸟款?或者悄悄的用了?
深層響應(yīng)式代理:reactive
說(shuō)了半天茂卦,終于進(jìn)入正題了何什。
reactive 會(huì)返回對(duì)象的響應(yīng)式代理,這種響應(yīng)式轉(zhuǎn)換是深層的等龙,可以影響所有的嵌套對(duì)象处渣。
注意:返回的是 object 的代理,他們的地址是相同的而咆,并沒(méi)有對(duì)object進(jìn)行clone(克禄舯取),所以修改代理的屬性值暴备,也會(huì)影響原object的屬性值悠瞬;同時(shí),修改原object的屬性值,也會(huì)影響reactive返回的代理的屬性值罩阵,只是代理無(wú)法攔截直接對(duì)原object的操作穿仪,所以模板不會(huì)有變化。
這個(gè)問(wèn)題并不明顯凌外,因?yàn)槲覀円话悴粫?huì)先定義一個(gè)object,然后再套上reactive涛浙,而是直接定義一個(gè) reactive康辑,這樣也就“不存在”原 object 了摄欲,但是我們要了解一下原理。
我們先定義一個(gè) reactive 實(shí)例疮薇,然后運(yùn)行看結(jié)果胸墙。
// js對(duì)象
const person = {
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
}
// person 的 reactive 代理 (驗(yàn)證地址是否相同)
const personReactive = reactive(person)
// js 對(duì)象 的 reactive 代理 (一般用法)
const objectReactive = reactive({
name: 'jykReactive',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
// 查看 reactive 實(shí)例結(jié)構(gòu)
console.log('reactive', objectReactive )
// 獲取嵌套對(duì)象屬性
const contacts = objectReactive .contacts
// 因?yàn)樯顚禹憫?yīng),所以依然有響應(yīng)性
console.log('contacts屬性:', contacts)
// 獲取簡(jiǎn)單類(lèi)型的屬性
let name = objectReactive.name
// name屬性是簡(jiǎn)單類(lèi)型的按咒,所以失去響應(yīng)性
console.log('name屬性:', name)
運(yùn)行結(jié)果:
Handler:可以看到 Vue 除重寫(xiě) set 和 get 外迟隅,還重寫(xiě)了deleteProperty、has和ownKeys励七。
Target: 指向一個(gè)Object智袭,這是建立reactive實(shí)例時(shí)的對(duì)象。
屬性的結(jié)構(gòu):
然后再看一下兩個(gè)屬性的打印結(jié)果掠抬,因?yàn)?contacts 屬性是嵌套的對(duì)象吼野,所以單獨(dú)拿出來(lái)也是具有響應(yīng)性的。
而 name 屬性由于是 string 類(lèi)型剿另,所以單獨(dú)拿出來(lái)并不會(huì)自動(dòng)獲得響應(yīng)性箫锤,如果單獨(dú)拿出來(lái)還想保持響應(yīng)性的話,可以使用toRef雨女。
注意:如果在模板里面使用{{personReactive.name}}的話谚攒,那么也是有響應(yīng)性的,因?yàn)檫@種用法是獲得對(duì)象的屬性值氛堕,可以被Proxy代理攔截馏臭,所以并不需要使用toRef。
如果想在模板里面直接使用{{name}}并且要具有響應(yīng)性讼稚,這時(shí)才需要使用toRef括儒。
reactive 相關(guān)函數(shù)放在下一篇介紹。
源碼:
GitHub總是上不去锐想,所以搬到gitee了帮寻。
https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi
在線演示:
https://naturefw.gitee.io/nf-vue-cdn/cdn/project-compositionapi/