全面瞭解Vue3的 reactive 和相關函式
阿新 • • 發佈:2021-04-01
Vue3的 reactive 怎麼用,原理是什麼,官網上和reactive相關的那些函式又都是做什麼用處的?這裡會一一解答。
# ES6的Proxy
Proxy 是 ES6 提供的一個可以攔截物件基礎操作的代理。因為 reactive 採用 Proxy 代理的方式,實現引用型別的響應性,所以我們先看看 Proxy 的基礎使用方法,以便於我理解 reactive 的結構。
我們先來定義一個函式,瞭解一下 Proxy 的基本使用方式:
```js
// 定義一個函式,傳入物件原型,然後建立一個Proxy的代理
const myProxy = (_target) => {
// 定義一個 Proxy 的例項
const proxy = new Proxy(_target, {
// 攔截 get 操作
get: function (target, key, receiver) {
console.log(`getting ${key}!`, target[key])
// 用 Reflect 呼叫原型方法
return Reflect.get(target, key, receiver)
},
// 攔截 set 操作
set: function (target, key, value, receiver) {
console.log(`setting ${key}:${value}!`)
// 用 Reflect 呼叫原型方法
return Reflect.set(target, key, value, receiver)
}
})
// 返回例項
return proxy
}
// 使用方法,是不是和reactive有點像?
const testProxy = myProxy({
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
console.log('自己定義的Proxy例項:')
console.log(testProxy)
// 測試攔截情況
testProxy.name = '新的名字' // set操作
console.log(testProxy.name) // get 操作
```
Proxy 有兩個引數 target 和 handle。
* target:要代理的物件,也可以是陣列,但是不能是基礎型別。
* handler:設定要攔截的操作,這裡攔截了 set 和 get 操作,當然還可以攔截其他操作。
我們先來看一下執行結果:
![自己寫的 Proxy 例項的執行結果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/700c3b49ba6141cc9e2d786f6339bad9~tplv-k3u1fbpfcp-zoom-1.image)
* Handler 可以看到我們寫的攔截函式 get 和 set;
* Target 可以看到物件原型。
> 注意:這裡只是實現了 get 和 set 的攔截,並沒有實現資料的雙向繫結,模板也不會自動更新內容,Vue內部做了很多操作才實現了模板的自動更新功能。
# 用 Proxy 給 reactive 套個娃,會怎麼樣?
有個奇怪的地方,既然 Proxy 可以實現對 set 等操作的攔截,那麼 reactive 為啥不返回一個可以監聽的鉤子呢?為啥要用 watch 來實現監聽的工作?
為啥會這麼想?因為看到了 Vuex4.0 的設計,明明已經把 state 整體自動變成了 reactive 的形式,那麼為啥還非得在 mutations 裡寫函式,實現 set 操作呢?好麻煩的樣子。
外部直接對 reactive 進行操作,然後 Vuex 內部監聽一下,這樣大家不就都省事了嗎?要實現外掛功能,還是跟蹤功能,不都是可以自動實現了嘛。
所以我覺得還是可以套個娃的。
## 實現模板的自動重新整理
本來以為上面那個 myProxy 函式,傳入一個 reactive 之後,就可以自動實現更新模板的功能了,結果模板沒理我。
這不對呀,我只是監聽了一下,不是又交給 reactive 了嗎?為啥模板不理我?
經過各種折騰,終於找到了原因,於是函式改成了這樣:
```js
/**
* 用 Proxy定義一個 reactive 的套娃,實現可以監聽任意屬性變化的目的。(不包含巢狀物件的屬性)
* @param {*} _target 要攔截的目標
* @param {*} callback 屬性變化後的回撥函式
*/
const myReactive = (_target, callback) => {
let _change = (key, value) => {console.log('內部函式')}
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])
}
// 呼叫原型方法
return Reflect.get(target, key, receiver)
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}:${value}!`)
// 源頭監聽
if (typeof callback === 'function') {
callback(key, value)
}
// 任意位置監聽
if (typeof _target.__watch === 'function') {
_change(key, value)
}
// 呼叫原型方法
return Reflect.set(target, key, value, target) // 這裡有變化,最後一個引數改成 target
}
})
// 實現任意位置的監聽,
proxy.__watch = (callback) => {
if (typeof callback === 'function') {
_change = callback
}
}
// 返回例項
return proxy
}
```
程式碼稍微多了一些,我們一塊一塊看。
* get
這裡要做一下 symbol 的判斷,否則會報錯。好吧,其實我們似乎不需要 console.log。
* set
這裡改了一下最後一個引數,這樣模板就可以自己更新了。
* 設定 callback 函式,實現源頭監聽
設定一個回撥函式,才能在攔截到set操作的時候,通知外部的呼叫者。只是這樣只適合於定義例項的地方。那麼接收引數的地方怎麼辦呢?
呼叫方法如下:
```js
// 定義一個攔截reactive的Proxy
// 並且實現源頭的監聽
const myProxyReactive = myReactive(retObject,
((key, value) =>{
console.log(`ret外部獲得通知:${key}:${value}`)
})
)
```
這樣我們就可以在回撥函式裡面得到修改的屬性名稱,以及屬性值。
這樣我們做狀態管理的時候,是不是就不用特意去寫 mutations 裡面的函數了呢?
* 內部設定一個鉤子函式
設定一個 _change() 鉤子函式,這樣接收引數的地方,可以通過這個鉤子來得到變化的通知。
呼叫方法如下:
```js
// 任意位置的監聽
myProxyReactive.__watch((key, value) => {
console.log(`任意位置的監聽:${key}:${value}`)
})
```
只是好像哪裡不對的樣子。
首先這個鉤子沒找到合適的地方放,目前放在了原型物件上面,就是說破壞了原型物件的結構,這個似乎會有些影響。
然後,接收引數的地方,不是可以直接得到修改的情況嗎?是否還需要做這樣的監聽?
> 最後,好像沒有 watch 的 deep 監聽來的方便,那麼問題又來了,為啥 Vuex 不用 watch 呢?或者悄悄的用了?
# 深層響應式代理:reactive
說了半天,終於進入正題了。
reactive 會返回物件的響應式代理,這種響應式轉換是深層的,可以影響所有的巢狀物件。
> 注意:返回的是 object 的代理,他們的地址是相同的,並沒有對object進行clone(克隆),所以修改代理的屬性值,也會影響原object的屬性值;同時,修改原object的屬性值,也會影響reactive返回的代理的屬性值,只是代理無法攔截直接對原object的操作,所以模板不會有變化。
這個問題並不明顯,因為我們一般不會先定義一個object,然後再套上reactive,而是直接定義一個 reactive,這樣也就“不存在”原 object 了,但是我們要了解一下原理。
我們先定義一個 reactive 例項,然後執行看結果。
```js
// js物件
const person = {
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
}
// person 的 reactive 代理 (驗證地址是否相同)
const personReactive = reactive(person)
// js 物件 的 reactive 代理 (一般用法)
const objectReactive = reactive({
name: 'jykReactive',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
// 檢視 reactive 例項結構
console.log('reactive', objectReactive )
// 獲取巢狀物件屬性
const contacts = objectReactive .contacts
// 因為深層響應,所以依然有響應性
console.log('contacts屬性:', contacts)
// 獲取簡單型別的屬性
let name = objectReactive.name
// name屬性是簡單型別的,所以失去響應性
console.log('name屬性:', name)
```
執行結果:
![reactive的列印結果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f3f218d3b3248d3b7533eb2ff02d087~tplv-k3u1fbpfcp-zoom-1.image)
* Handler:可以看到 Vue 除重寫 set 和 get 外,還重寫了deleteProperty、has和ownKeys。
* Target: 指向一個Object,這是建立reactive例項時的物件。
屬性的結構:
![reactive的屬性列印結果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47d127cbfbd7471a90af570b346a5d50~tplv-k3u1fbpfcp-zoom-1.image)
然後再看一下兩個屬性的列印結果,因為 contacts 屬性是巢狀的物件,所以單獨拿出來也是具有響應性的。
而 name 屬性由於是 string 型別,所以單獨拿出來並不會自動獲得響應性,如果單獨拿出來還想保持響應性的話,可以使用toRef。
> 注意:如果在模板裡面使用{{personReactive.name}}的話,那麼也是有響應性的,因為這種用法是獲得物件的屬性值,可以被Proxy代理攔截,所以並不需要使用toRef。
如果想在模板裡面直接使用{{name}}並且要具有響應性,這時才需要使用toRef。
# 淺層響應式代理:shallowReactive
有的時候,我們並不需要巢狀屬性也具有響應性,這時可以使用shallowReactive 來獲得淺層的響應式代理,這種方式只攔截自己的屬性的操作,不涉及巢狀的物件屬性的操作。
```js
const personShallowReactive = shallowReactive({
name: 'jykShallowReactive',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
// 檢視 shallowReactive 例項結構
console.log('shallowReactive', objectShallowReactive)
// 獲取巢狀物件屬性
const contacts = objectShallowReactive.contacts
// 因為淺層代理,所以沒有響應性
console.log('contacts屬性:', contacts)
// 獲取簡單型別的屬性
let name = objectShallowReactive.name
// 因為淺層代理且簡單型別,所以失去響應性
console.log('name屬性:', name)
```
![shallowReactive的列印結果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/307663536e144598be3bd7bc9a6d291b~tplv-k3u1fbpfcp-zoom-1.image)
shallowReactive 也是用 Proxy 實現響應性的,而單獨使用contacts屬性並沒有響應性,因為 shallowReactive 是淺層代理,所以不會讓巢狀物件獲得響應性。
> 注意:objectShallowReactive.contacts.QQ = 123 ,這樣修改屬性也是沒有響應性的。
單獨使用的屬性的形式:
![shallowReactive的屬性](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c9ffbb6e88d94a4ba1e9018a4daa1aff~tplv-k3u1fbpfcp-zoom-1.image)
巢狀物件和name屬性,都沒有變成響應式。
# 做一個不允許響應的標記:markRaw
有的時候我們不希望js物件變成響應式的,這時我們可以用markRaw 做一個標記,這樣即使使用 reactive 也不會變成響應式。
如果確定某些資料是不會變化的,那麼也就不用變成響應式,這樣可以節省一些不必要的效能開銷。
```js
// 標記js物件
const object = markRaw({
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
// 試圖對標記的物件做相應性代理
const retObject2 = reactive(object)
// 使用物件的屬性做相應性代理
const retObject1 = reactive({
name: object.name
})
console.log('作為初始值:', retObject1) // 無法變成響應性代理
console.log('無法變成響應式:', retObject2) // 可以變成響應性代理
```
執行結果:
![markRaw的列印結果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2fdd971579946c48c47eb38c94bbbb6~tplv-k3u1fbpfcp-zoom-1.image)
做標記後的js物件作為引數,不會變成響應式,但是使用屬性值作為引數,還是可以變成響應式。
那麼哪些地方可以用到呢?我們可以在給元件設定(**引用型別的**)屬性的時候使用,預設情況下元件的屬性都是自帶響應性的,但是如果父元件裡設定給子元件的屬性值永遠不會發生變化,那麼還變成響應式的話,就有點浪費效能的嫌疑了。
如果想節約一下的話,可以在父元件設定屬性的時候加上markRaw標記。
# 深層只讀響應式代理:readonly
有的時候雖然我們想得到一個響應式的代理,但是隻想被讀取,而不希望被修改(比如元件的props,元件內部不希望被修改),那麼這時候我們可以用readonly。
readonly可以返回object、reactive或者ref的深層只讀代理,我們來分別測試一下:
```js
// object的只讀響應代理
const objectReadonly = readonly(person)
// reactive 的只讀響應代理
const reactiveReadonly = readonly(objectReactive)
// 檢視 readonly 例項結構
console.log('object 的readonly', objectReadonly)
console.log('reactive 的readonly', reactiveReadonly)
// 獲取巢狀物件屬性
const contacts = reactiveReadonly.contacts
console.log('contacts屬性:', contacts) // 因為深層響應,所以依然有響應性
// 獲取簡單型別的屬性
let name = reactiveReadonly.name
console.log('name屬性:', name) // 屬性是簡單型別的,所以失去響應性
```
執行結果:
![object的readonly](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/779808efb91c4c4a9fb7ea9fdabb0d5a~tplv-k3u1fbpfcp-zoom-1.image)
* Handler,明顯攔截的函式變少了,set的引數也變少了,點進去看原始碼,也僅僅只有一行返回警告的程式碼,這樣實現攔截設定屬性的操作。
* Target,指向object。
執行結果:
![reactive的readonly](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8feaa699b6de4ca98a27dede2bd883c6~tplv-k3u1fbpfcp-zoom-1.image)
* Handler,這部分是一樣的。
* Target,指向的不是object,而是一個Proxy代理,也就是reactive。
# 淺層只讀響應代理:shallowReadonly
和readonly相對應,shallowReadonly是淺層的只讀響應代理,和readonly的使用方式一樣,只是不會限制巢狀物件只讀。
```js
// object 的淺層只讀代理
const objectShallowReadonly = shallowReadonly(person)
// reactive 的淺層只讀代理
const reactiveShallowReadonly = shallowReadonly(objectReactive)
```
shallowReadonly的結構和 readonly 的一致,就不貼截圖了。
# 獲取原型:toRaw
toRaw 可以獲取 Vue 建立的代理的原型物件,但是不能獲取我們自己定義的Proxy的例項的原型。
toRaw大多是在Vue內部使用,目前只發現在向indexedDB裡面寫入資料的時候,需要先用 toRaw 取原型,否則會報錯。
```js
// 獲取reactive、shallowReactive、readonly、shallowReadonly的原型
console.log('深層響應的原型', toRaw(objectReactive))
console.log('淺層響應的原型', toRaw(objectShallowReactive))
console.log('深層只讀的原型', toRaw(objectReadonly))
console.log('淺層只讀的原型', toRaw(objectShallowReadonly))
```
執行結果都是普通的object,就不貼截圖了。
# 型別判斷
Vue提供了三個用於判斷型別的函式:
* isProxy:判斷物件是否是Vue建立的Proxy代理,包含reactive、readonly、shallowReactive和shallowReadonly建立的代理,但是不會判斷自己寫的Proxy代理。
* isReactive:判斷是否是reactive建立的代理。如果readonly的原型是reactive,那麼也會返回true。
* isReadonly:判斷是否是readonly、shallowReadonly建立的代理。這個最簡單,只看代理不看target。
我們用這三個函式判斷一下我們上面定義的這些Proxy代理,看看結果如何。
我們寫點程式碼對比一下:
```js
const myProxyObject = myProxy({title:'222', __v_isReactive: false})
console.log('myProxyObject', myProxyObject)
const myProxyReactive = myProxy(objectReactive)
console.log('myProxyReactive', myProxyReactive)
// 試一試 __v_isReadonly
console.log('objectReactive', objectReactive)
console.log('__v_isReadonly'
, objectReactive.__v_isReadonly
, objectReactive.__v_isReactive
)
return {
obj: { // js物件
check1: isProxy(person),
check2: isReactive(person),
check3: isReadonly(person)
},
myproxy: { // 自己定義的Proxy object
check1: isProxy(myProxyObject),
check2: isReactive(myProxyObject),
check3: isReadonly(myProxyObject)
},
myproxyReactive: { // 自己定義的Proxy reactive
check1: isProxy(myProxyReactive),
check2: isReactive(myProxyReactive),
check3: isReadonly(myProxyReactive)
},
// 深層響應 reactive(object)
reto: { // reactive(object)
check1: isProxy(objectReactive),
check2: isReactive(objectReactive),
check3: isReadonly(objectReactive)
},
// 淺層響應 引數:object
shallowRetObj: {
check1: isProxy(objectShallowReactive),
check2: isReactive(objectShallowReactive),
check3: isReadonly(objectShallowReactive)
},
// 淺層響應 引數:reactive
shallowRetRet: {
check1: isProxy(objectShallowReactive),
check2: isReactive(objectShallowReactive),
check3: isReadonly(objectShallowReactive)
},
// 深層只讀,引數 object =======================
readObj: { // readonly object
check1: isProxy(objectReadonly),
check2: isReactive(objectReadonly),
check3: isReadonly(objectReadonly)
},
// 深層只讀,引數 reactive
readRet: { // readonly reactive
check1: isProxy(reactiveReadonly),
check2: isReactive(reactiveReadonly),
check3: isReadonly(reactiveReadonly)
},
// 淺層只讀 引數:object
shallowReadObj: {
check1: isProxy(objectShallowReadonly),
check2: isReactive(objectShallowReadonly),
check3: isReadonly(objectShallowReadonly)
},
// 淺層只讀 引數:reactive
shallowReadRet: {
check1: isProxy(reactiveShallowReadonly),
check2: isReactive(reactiveShallowReadonly),
check3: isReadonly(reactiveShallowReadonly)
},
person
}
```
對比結果:
![驗證型別的對比測試](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9c8b93795af44c6a515aea37a89b079~tplv-k3u1fbpfcp-zoom-1.image)
## 總結一下:
* isReadonly 最簡單,只有readonly、shallowReadonly建立的代理才會返回 true,其他的都是 false。
* isProxy也比較簡單,Vue建立的代理才會返回true,如果是自己定義的Proxy,要看原型是誰,如果原型是 reactive(包括其他三個)的話,也會返回true。
* isReactive就有點複雜,reactive 建立的代理會返回 true,其他的代理(包含自己寫的)還要看一下原型,如果是 reactive 的話,也會返回true。
## 判斷依據
那麼這三個函式是依據什麼判斷的呢?自己做的 Proxy 無意中監控到了“__v_isReactive”,難道是隱藏屬性?測試了一下,果然是這樣。
myProxy({title:'測試隱藏屬性', __v_isReactive: true}),這樣定義一個例項,也會返回true。
# reactive直接賦值的方法
使用的時候我們會發現一個問題,如果直接給 reactive 的例項賦值的話,就會“失去”響應性,這個並不是因為 reactive 失效了,而是因為 setup 只會執行一次,return也只有一次給模板提供資料(地址)的機會,模板只能得到一開始提供的 reactive 的地址,如果後續直接對 reactive 的例項賦值操作,會覆蓋原有的地址,產生一個新的Proxy代理地址,然而模板並不會得到這個新地址,還在使用“舊”地址,因為無法獲知新地址的存在,所以模板不會有變化。
那麼就不能直接賦值了嗎?其實還是有方法的,只需要保證地址不會發生變化即可。
## 物件的整體賦值的方法。
有請 ES6 的 Object.assign 登場,這個方法是用來合併兩個或者多個物件的屬性的,如果屬性名稱相同後面的屬性會覆蓋前面的屬性。所以大家在使用的時候要謹慎使用,確保兩個物件的屬性就相容的,不會衝突。
程式碼如下:
```js
Object.assign(objectReactive, {name: '合併', age: 20, newProp: '新屬性'})
```
## 陣列的整體賦值的方法。
陣列就方便多了,可以先清空再 push 的方式,程式碼如下:
```js
// retArray.length = 0 // 這裡清空的話,容易照成閃爍,所以不要急
setTimeout(() => {
const newArray = [
{ name: '11', age: 18 },
{ name: '22', age: 18 }
]
// 等到這裡再清空,就不閃爍了。
retArray.length = 0
retArray.push(...newArray)
}, 1000)
```
# var 和 let、const
ES6 新增了 let 和 const,那麼我們應該如何選擇呢?
簡單的說,var不必繼續使用了。
let 和 const 的最大區別就是,前者是定義“變數”的,後者是定義“常量”的。
可能你會覺得奇怪,上面的程式碼都是用const定義的,但是後續程式碼都是各種改呀,怎麼就常量了?其實const判斷的是,地址是否改變,只要地址不變就可以。
對於基礎型別,值變了地址就變了;而對於引用型別來說,改屬性值的話,物件地址是不會發生變化的。
而 const 的這個特點整合可以用於保護 reactive 的例項。由Vue的機制決定,reactive的例項的地址是不可以改變的,變了的話模板就不會自動更新,const可以確保地址不變,變了會報錯(開發階段需要eslint支援)。
於是const和reactive(包括 ref 等)就成了絕配。
# 原始碼:
https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi
# 線上演示:
https://naturefw.gitee.io/nf-vue-cdn/cdn/project-composi