1. 程式人生 > >全面瞭解Vue3的 reactive 和相關函式

全面瞭解Vue3的 reactive 和相關函式

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