vue3原始值響應(yīng)方案及響應(yīng)丟失問題解讀
前言
我們了解到非原始值是使用 proxy 進(jìn)行代理的,但是 proxy 無法對(duì) 原始值進(jìn)行代理。vue2中 object.defineproperty 也無法直接截取原始值,但是我們是將數(shù)據(jù)放入 data 中,對(duì) data 整個(gè)對(duì)象進(jìn)行代理,自然就使得 data 中的原始值也能進(jìn)行響應(yīng)。
但是 vue3 沒有 data 選項(xiàng),哪是怎么實(shí)現(xiàn)原始值響應(yīng)的?這就要介紹我們要學(xué)的內(nèi)容了。
注意:下面的實(shí)現(xiàn)原理并非是真正的源碼,可以看做是極簡后的源碼核心實(shí)現(xiàn)。這樣做的目的是如果放入源碼,源碼非常多且雜,不能一目了然看出其核心原理實(shí)現(xiàn)。
一、ref 的引入
ref 就是解決 proxy 無法直接代理原始值的問題。我們先來看 ref 的使用:
const name = ref('小黑子')
ref 是怎么實(shí)現(xiàn)的呢?其實(shí)就是用對(duì)象“包裹”原始值。我們?cè)賮砜匆幌?ref 的實(shí)現(xiàn):
function ref(val){ ? ? // 使用對(duì)象包裹原始值 ? ? const wrapper = { ? ? ? ? value:val ? ? ? ? } ? ? // 利用 reactive 將對(duì)象變成響應(yīng)式數(shù)據(jù) ? ? return reactive(wrapper) }
ref 的實(shí)現(xiàn)就是這么簡單。
ref 對(duì)原始值響應(yīng)主要就做了這兩件事:
- 1、使用對(duì)象包裹原始值。
- 2、使用 reactive 將包裹對(duì)象變?yōu)轫憫?yīng)式數(shù)據(jù)。
二、isref 的實(shí)現(xiàn)
我們使用 ref 創(chuàng)建一個(gè)響應(yīng)式對(duì)象,但是我們要怎么區(qū)別一個(gè)對(duì)象是普通對(duì)象還是 ref 對(duì)象呢?于是我們的 isref 出現(xiàn)了。
我們看一下它的使用:
const name = ref('cj') console.log(isRef(name)); // true
那么它的實(shí)現(xiàn)原理是怎樣的呢?主要實(shí)現(xiàn)還是在 ref API內(nèi)部,我們來看一下具體實(shí)現(xiàn)代碼:
function ref(val){ ? ? const wrapper = { ? ? ? ? value:val ? ? ? ? } ? ? Object.defineProperty(warpper,'__v_isRef',{ ? ? ? ? value:true ? ? ? ? }) ? ? return reactive(wrapper) }
原來就是在 ref 內(nèi)部給 包裹對(duì)象添加一個(gè)不可枚舉不可寫的屬性,并且值為 true 。這樣我們就可以檢查該屬性來判斷是不是 ref 了。
function isRef(val) { ? ? return val.__v_isRef ?? false }
三、響應(yīng)丟失
什么是響應(yīng)丟失?響應(yīng)丟失就是響應(yīng)式數(shù)據(jù)不進(jìn)行響應(yīng)了。我們來看下方代碼:
const obj = reactive({foo:1,bar:2}) const {foo,bar} = obj obj.foo++ ? ?// foo不會(huì)改變,還是 1
上面的 obj 已經(jīng)響應(yīng)丟失了,也就不會(huì)觸發(fā)重新渲染。為什么會(huì)這樣呢?其實(shí)就是因?yàn)槭褂昧私Y(jié)構(gòu)賦值,展開運(yùn)算符也會(huì)使其失效。
const obj = reactive({foo:1,bar:2}) const newObj = {...obj} obj.foo++ ? // newObj.foo不會(huì)改變,還是 1
這就相當(dāng)于重新定義了新的數(shù)據(jù),而不再是原來的響應(yīng)數(shù)據(jù)了,自然也就不具有響應(yīng)式能力。
1、toRef登場
toRef 就是為了解決響應(yīng)丟失的問題。我們來看一下它的實(shí)現(xiàn):
function toRef(obj,key) { ? ? const wrapper = { ? ? ? ? get value() { ? ? ? ? ? ? return obj[key] ? ? ? ? ? ? ? ? }, ? ? ? ? set value(val) { ? ? ? ? ? ? obj[key] = val ? ? ? ? } ? ? ? ? } ? ? Object.defineProperty(wrapper,'__v_isRef',{ ? ? ? ? value:true ? ? ? ? }) ? ? return wrapper }
傳入兩個(gè)參數(shù),第一個(gè)是響應(yīng)式數(shù)據(jù),第二個(gè)是 obj 的一個(gè)鍵。
- 第一部分就是設(shè)置聲明一個(gè)對(duì)象,對(duì)象先設(shè)置了 value 屬性的 get 用于將訪問 toRef 值時(shí),將返回傳入的響應(yīng)式數(shù)據(jù)對(duì)應(yīng)的屬性值,然后設(shè)置了 value屬性的 set 用于將設(shè)置 toRef 值時(shí),就拿取設(shè)置的新值更新響應(yīng)式數(shù)據(jù)對(duì)應(yīng)的屬性值。也就是說,toRef 返回的對(duì)象還是利用的響應(yīng)式數(shù)據(jù)。
- 第二部分用于設(shè)置返回的數(shù)據(jù)是 ref 數(shù)據(jù)。因?yàn)?toRef 返回的數(shù)據(jù)類似 ref 數(shù)據(jù),為了統(tǒng)一就直接認(rèn)定為是一個(gè) ref 數(shù)據(jù)。
- 第三部分就是返回響應(yīng)式數(shù)據(jù)對(duì)應(yīng)聲明的屬性對(duì)象
這樣 toRef 就解決了響應(yīng)丟失的問題。
2、toRefs 加入
toRefs 就是將整個(gè)響應(yīng)式對(duì)象進(jìn)行解構(gòu)響應(yīng)化。實(shí)現(xiàn)代碼如下:
function toRefs() { ? ? const ret = {} ? ? for (const key in obj) { ? ? ? ? ret[key] = toRef(obj,key) ? ? ? ? } ? ? return ret }
使用 for 循環(huán)逐一對(duì)屬性進(jìn)行轉(zhuǎn)換。這下我們?cè)賮砜匆幌率褂茫?/p>
const obj = reactive({foo:1,bar:2}) const {foo,bar} = toRefs(obj) obj.foo++ ? ?// foo.value變?yōu)?了
3、詭異的其它響應(yīng)式丟失情況
當(dāng)屬性為非原始值的時(shí)候,解構(gòu)之后還是依然能響應(yīng)
const obj = reactive({foo:{age:18},bar:2}) const {foo,bar} = obj obj.foo.age++ ? ?// foo.age變?yōu)?了 obj.bar++ ? ? ? // bar沒有改變,還是1
這是為什么?原因其實(shí)很簡單,因?yàn)榉窃贾蒂x值的是引用地址,也就是說解構(gòu)后的變量其實(shí)還是指向原響應(yīng)式數(shù)據(jù)的屬性。而原始值就是單純的賦值,就不會(huì)進(jìn)行響應(yīng)。
reactive 響應(yīng)數(shù)據(jù)重新賦值后不再響應(yīng),ref 響應(yīng)數(shù)據(jù)賦值后依然響應(yīng) let obj1 = reactive({foo:1,bar:2}) let obj2 = ref({foo:1,bar:2}) ? // 假如 obj1 與 obj2 直接展示在頁面上 obj1 = {boo:2,far:3} ? ?// 頁面 obj1 還是 {foo:1,bar:2} obj2.value = {boo:2,far:3} ? ?// 頁面 obj2 變?yōu)?{boo:2,far:3} 了?
這又是什么原因?reactive 重新賦值響應(yīng)丟失,就是重新賦值了新的對(duì)象,自然就成為普通數(shù)據(jù)了,不再響應(yīng)。而 ref 還是能響應(yīng),是因?yàn)?ref 在內(nèi)部進(jìn)行 set 處理。代碼如下:
function ref(val){ ? ? const wrapper = { ? ? ? ? value:val ? ? ? ? set value(val) { ? ?// isObject 這里代表判斷是否是非原始值的一個(gè)方法 ? ? ? ? ? ? ?value = isObject(val) === 'Object' ? reactive(val) : val ? ? ?? ? ? ? ? } ? ? } ? ? Object.defineProperty(warpper,'__v_isRef',{ ? ? ? ? value:true ? ? ? ? }) ? ? return reactive(wrapper) }
我們明白了,其實(shí) ref 在 set 中判斷了設(shè)置的新值是否是非原始值,如果是就調(diào)用 reactive 將其變?yōu)轫憫?yīng)式數(shù)據(jù)。
四、unref 自動(dòng)脫 ref
我們?cè)谑褂?ref 響應(yīng)式數(shù)據(jù)時(shí),會(huì)覺得總是需要 .value 來獲取值,增加了用戶的心智負(fù)擔(dān)。
那可不可以不通過 .value 訪問值,而時(shí)直接就能夠訪問值呢?
這樣用于也不用關(guān)心某個(gè)數(shù)據(jù)到底是不是 ref 數(shù)據(jù),需不需要通過 value 屬性去獲取值。
這就到了我們的 unref 出手了。unref 實(shí)現(xiàn)了自動(dòng)脫 ref 能力,自動(dòng)脫 ref 就是如果讀取的屬性是一個(gè) ref,則直接將該 ref 對(duì)應(yīng)的 value 屬性值返回。
我們來看一下 unref 的實(shí)現(xiàn):
function unref(target) { ? ? return new Proxy(target,{ ? ? ? ? get(target,key,receiver) { ? ? ? ? ? ? const value = Reflect.get(target,key,receiver) ? ? ? ? ? ? return value.__v_isRef ? value.value : value ? ? ? ? ? ? ? ? }, ? ? ? ? set(target,key,newValue,receiver) { ? ? ? ? ? ? const value = target[key] ? ? ? ? ? ? if (value.__v_isRef) { ? ? ? ? ? ? ? ? value.value = newValue ? ? ? ? ? ? ? ? return true ? ? ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? return Reflect.set(target,key,newValue,receiver) ? ? ? ? } ? ? }) }
我們發(fā)現(xiàn) unref 內(nèi)部使用 Proxy 代理了目標(biāo)對(duì)象,接收一個(gè)對(duì)象作為參數(shù),并返回該對(duì)象的代理對(duì)象。當(dāng)我們?cè)L問 unref 的數(shù)據(jù)時(shí),觸發(fā) get 捕獲器,然后再捕獲器內(nèi)部判斷了傳入對(duì)象是否是 ref 對(duì)象,如果是就直接返回 ref 的 .value 值。如果不是則直接返回代理對(duì)象。
當(dāng)對(duì) unref 返回的代理對(duì)象設(shè)置值時(shí),觸發(fā) set 捕獲器,如果代理的對(duì)象時(shí) ref ,就將需要設(shè)置的新值賦值給 .value,不是則直接進(jìn)行賦值處理。
知識(shí)擴(kuò)展
當(dāng)我們使用 ref 創(chuàng)建響應(yīng)式數(shù)據(jù)后,將其在模板中展示,為什么不用 .value 了。
<script setup lang="ts"> import { ref } from 'vue' const name = ref('小黑子') </script> ? <template> ? <div>{{ name }}</div> </template>
其實(shí)原因很簡單,在組件 setup 中聲明的 ref 響應(yīng)式數(shù)據(jù)會(huì)傳遞給 unref 函數(shù)進(jìn)行處理。所以在模板中訪問 ref 的值,無需通過 value 屬性來訪問。
我們使用的 reactive 其實(shí)也是有 自動(dòng)脫 ref 功能的,看一下下方例子:
const count = ref(0) const obj = reactive({conut}) ? obj.count ? ?// 0? ?
我們可以看見 obj.count 是一個(gè) ref 響應(yīng)式數(shù)據(jù)。在 count 外層包裹一層對(duì)象,再傳遞給 reactive 后,再訪問 obj.count 時(shí)就不需要再通過 value 屬性訪問值了。
也正是因?yàn)?reactive 內(nèi)部也同樣實(shí)現(xiàn)了自動(dòng)脫 ref 的能力。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue使用formData時(shí)候傳遞參數(shù)是個(gè)空值的情況處理
這篇文章主要介紹了vue使用formData時(shí)候傳遞參數(shù)是個(gè)空值的情況處理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05el-table-column 內(nèi)容不自動(dòng)換行的解決方法
本文主要介紹了el-table-column 內(nèi)容不自動(dòng)換行的解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08vue動(dòng)態(tài)綁定v-model屬性名方式
這篇文章主要介紹了vue動(dòng)態(tài)綁定v-model屬性名方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08Vue 讓元素抖動(dòng)/擺動(dòng)起來的實(shí)現(xiàn)代碼
這篇文章主要介紹了Vue 讓元素抖動(dòng)/擺動(dòng)起來的實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05vue基于element-ui的三級(jí)CheckBox復(fù)選框功能的實(shí)現(xiàn)代碼
最近vue項(xiàng)目需要用到三級(jí)CheckBox復(fù)選框,需要實(shí)現(xiàn)全選反選不確定三種狀態(tài)。這篇文章主要介紹了vue基于element-ui的三級(jí)CheckBox復(fù)選框功能的實(shí)現(xiàn)方法,需要的朋友可以參考下2018-10-10vue實(shí)現(xiàn)a標(biāo)簽點(diǎn)擊高亮方法
下面小編就為大家分享一篇vue實(shí)現(xiàn)a標(biāo)簽點(diǎn)擊高亮方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-03-03