Vue3 糟糕透頂 Api——Reactivity
看到標(biāo)題,小伙伴們是不是很驚訝?也許會(huì)聯(lián)想到Reactivity是不是設(shè)計(jì)上有缺陷?其實(shí)是醉翁之意不在酒!Reactivity可以說(shuō)是vue3中最復(fù)雜的一個(gè)地方,當(dāng)然也是功能最強(qiáng)大的一個(gè)點(diǎn),聽起來(lái)是塊硬骨頭哈??,這讓我想起亮劍中李云龍面對(duì)小日本挑釁說(shuō)的話:就是閻王爺來(lái)了,我也得摟他幾根胡子下來(lái)。那么今天咱們也摟摟Reactivity的胡子。
在vue3當(dāng)中,它是把數(shù)據(jù)響應(yīng)式Api全都暴露出來(lái)了,在vue2中是沒(méi)有這樣做的,那vue2是怎么做的呢?它是將數(shù)據(jù)配置到data當(dāng)中,data中的數(shù)據(jù)會(huì)自動(dòng)變成響應(yīng)式數(shù)據(jù),我們把這個(gè)過(guò)程叫做注入,它會(huì)注入到組件實(shí)例當(dāng)中去,如下:
{
//data中的數(shù)據(jù)都是響應(yīng)式的,會(huì)被注入到組件實(shí)例當(dāng)中
data(){
return{
name:"法醫(yī)",
idol:"喬布斯",
publicNumber:"前端獵手"
}
}
}
然而在vue3中,它不再是data了,而是setup函數(shù),我們把它稱之為composition Api,我們要在setup函數(shù)中使用響應(yīng)式數(shù)據(jù),就不可避免需要暴露出響應(yīng)式的Api供我們使用,接下來(lái),我們來(lái)看看vue3提供了哪些跟數(shù)據(jù)響應(yīng)式相關(guān)的函數(shù)。
?? 獲取響應(yīng)式數(shù)據(jù)
可以獲取響應(yīng)式數(shù)據(jù)的api:
reactive
這個(gè)api會(huì)傳入一個(gè)
對(duì)象,然后返回一個(gè)對(duì)象代理proxy,并且它是可以深度代理對(duì)象中的所有成員的,也就是說(shuō)對(duì)象里面再嵌套一個(gè)對(duì)象,也是響應(yīng)式的數(shù)據(jù)舉個(gè)栗子??:
import { reactive } from'vue'
const state = reactive({name:"法醫(yī)",age:18})
window.state = state;效果展示:
image.png現(xiàn)在這個(gè)對(duì)象就變成響應(yīng)式的了,對(duì)對(duì)象進(jìn)行操作,vue就可以收到通知了
readonly
這個(gè)api需要傳入一個(gè)對(duì)象或者是一個(gè)代理,同樣也會(huì)返回一個(gè)對(duì)象代理,它只能讀取代理對(duì)象中的成員,而不能修改,也就是只能
get,不可以set,它也是可以深度代理對(duì)象中所有成員的。舉個(gè)栗子??:
import { reactive,readonly } from'vue'
const imState = readonly({name:"法醫(yī)",age:18})
window.imState = imState;效果展示:

看見沒(méi),報(bào)了一個(gè)錯(cuò)誤,提示說(shuō)要修改的目標(biāo)是只讀的,修改失敗
剛才有說(shuō)readonly可以傳一個(gè)對(duì)象,也可以傳一個(gè)代理,我們?cè)倏纯磦饕粋€(gè)代理會(huì)怎么樣
import { reactive,readonly } from'vue'
const state = reactive({name:"法醫(yī)",age:18})
const imState = readonly(state);//傳一個(gè)代理進(jìn)去
window.imState = imState;效果展示:

傳一個(gè)代理的時(shí)候也是不可以進(jìn)行賦值操作的,反正只要經(jīng)過(guò)
readonly后就只能讀不能賦值,但是,我們可以在readonly之前修改值,然后讓它進(jìn)行代理就可以了。??注意:
reactive和readonly,這兩個(gè)api是代理對(duì)象的,是沒(méi)辦法代理普通值的,會(huì)報(bào)錯(cuò)舉個(gè)栗子??:
import { reactive,readonly } from'vue'
const state = reactive("法醫(yī)")效果展示:

報(bào)了一個(gè)警告,原始值是沒(méi)法代理的,那么如果需要代理普通值,那該咋辦呢?那就要用到
refapi了ref
ref中可以傳入任何數(shù)據(jù),最終會(huì)將數(shù)據(jù)放到一個(gè)對(duì)象中的value中,比如這種{ value : ...},對(duì)value的訪問(wèn)是響應(yīng)式的,如果給value的值是一個(gè)對(duì)象的話,它會(huì)通過(guò)reactive函數(shù)進(jìn)行代理舉個(gè)栗子??:
import { reactive,readonly,ref } from'vue'
const state = ref({name:"法醫(yī)",age:18})
window.state = state;效果展示:

還有一種情況是:如果說(shuō)傳入的value值已經(jīng)是代理了,那么會(huì)直接使用代理
舉個(gè)栗子??:
import { reactive,readonly,ref } from'vue'
const state = reactive({name:"法醫(yī)",age:18});
const imState = ref(state)
window.imState = imState;效果展示:

computed
computed需要傳入一個(gè)函數(shù)
function,它返回的值跟ref一樣,也是{value:...},當(dāng)讀取value值的時(shí)候,它會(huì)根據(jù)情況決定是否要運(yùn)行函數(shù),這個(gè)情況就是有沒(méi)有用到這個(gè)函數(shù),并且它是有緩存的,當(dāng)依賴的響應(yīng)式數(shù)據(jù)沒(méi)有變化的時(shí)候,拿到的是緩存里面的值,只有當(dāng)state.name或者state.age發(fā)生改變時(shí)才會(huì)重新運(yùn)行。舉個(gè)栗子??:
import { reactive,readonly,ref ,computed} from'vue'
const state = reactive({name:"法醫(yī)",age:18})
const result = computed(()=>{
console.log("computed");
return state.name + state.age
})
console.log(result.value);效果展示:

反正以上四個(gè)api不管它怎么進(jìn)行處理,就一個(gè)目的,那就是把數(shù)據(jù)變成響應(yīng)式數(shù)據(jù)。
那么在開發(fā)中到底用哪個(gè)?
?? 注意:用
ref的時(shí)候要拿到數(shù)據(jù)必須是ref.value哈,不然拿不到,切記!??
- 如果想要一個(gè)對(duì)象變?yōu)轫憫?yīng)式數(shù)據(jù),可以使用
reactive或者ref - 如果說(shuō)讓對(duì)象所有屬性只能讀,就用
readonly - 如果想讓一個(gè)非對(duì)象數(shù)據(jù)變成響應(yīng)式數(shù)據(jù),就用
ref - 如果想要根據(jù)已知的響應(yīng)式數(shù)據(jù)得到一個(gè)新的響應(yīng)式數(shù)據(jù),就用
computed
?? 監(jiān)聽數(shù)據(jù)變化
- watchEffect
const stop = watchEffect(() => {
//watchEffect 函數(shù)會(huì)立即執(zhí)行,然后監(jiān)聽函數(shù)中會(huì)用到的響應(yīng)式數(shù)據(jù),響應(yīng)式數(shù)據(jù)變化后會(huì)再次執(zhí)行
})
//通過(guò)調(diào)用stop函數(shù)就會(huì)停止監(jiān)聽
stop();//停止監(jiān)聽
舉個(gè)栗子??:
import { reactive, ref, watchEffect } from"vue";
const state = reactive({a: 1,b: 2,});
const count = ref(0);
watchEffect(() =>{
console.log(state.a,count.value);//先會(huì)立即執(zhí)行一次
})
state.a++;//這里依賴數(shù)據(jù)改變?cè)俅螆?zhí)行一次
效果展示:

watchEffect函數(shù)之所以知道依賴變化了,是因?yàn)槔锩娴臄?shù)據(jù)是響應(yīng)式的,當(dāng)讀取數(shù)據(jù)的時(shí)候用的是get方法,get會(huì)收集依賴。還要注意的是如果說(shuō)依賴的數(shù)據(jù)同時(shí)改變很多次,最終結(jié)果是會(huì)顯示一次,因?yàn)檫\(yùn)行過(guò)程是異步的,是會(huì)到微隊(duì)列中執(zhí)行的,等數(shù)據(jù)變完之后才會(huì)運(yùn)行,如下例子:
舉個(gè)栗子??:
import { reactive, ref, watchEffect } from"vue";
const state = reactive({a: 1,b: 2,});
const count = ref(0);
watchEffect(() =>{
console.log(state.a,count.value);
})
//運(yùn)行多次
state.a++;
state.a++;
state.a++;
state.a++;
state.a++;
state.a++;
state.a++;
state.a++;
效果展示:

從最終結(jié)果可以看出最終只運(yùn)行兩次,一次是立即執(zhí)行,第二次是數(shù)據(jù)改變后
- watch
這個(gè)watch相當(dāng)于vue2的$watch,這個(gè)watch有點(diǎn)麻煩,因?yàn)樗枰謩?dòng)去指定監(jiān)控哪些值的變化,當(dāng)變化的時(shí)候,它會(huì)把新的值和舊的值同時(shí)給你
舉個(gè)栗子??:
import { reactive, ref, watch } from"vue";
const state = reactive({a: 1,b: 2,});
const count = ref(0);
watch(()=>state.a,(newValue,oldValue) =>{
console.log("新值",newValue,"舊值",oldValue);
})
state.a++;//修改依賴
效果展示:

?? 注意:在這里需要注意的是watch跟watchEffect不同的是watch不會(huì)立即運(yùn)行函數(shù),只有當(dāng)依賴的值改變時(shí)才會(huì)執(zhí)行,watch中會(huì)傳兩個(gè)參數(shù),在上面例子中傳的是()=>state.a,那為什么不直接傳state.a呢?如果直接傳state.a的話,那么相當(dāng)于傳了一個(gè)1進(jìn)去,這樣數(shù)據(jù)就不是響應(yīng)式的了,如下:
舉個(gè)栗子??:?直接傳state.a
import { reactive, ref, watch } from"vue";
const state = reactive({a: 1,b: 2,});
const count = ref(0);
watch(state.a,(newValue,oldValue) =>{
console.log("新值",newValue,"舊值",oldValue);
})
state.a++;//修改依賴
效果展示:

如果直接傳state.a,這里就會(huì)報(bào)警告,翻譯過(guò)來(lái)是:無(wú)效的監(jiān)視源:1監(jiān)視源只能是getter/effect函數(shù)、ref、被動(dòng)對(duì)象或這些類型的數(shù)組,意思就是說(shuō)這里參數(shù)只能是響應(yīng)式數(shù)據(jù)。
當(dāng)傳一個(gè)()=>state.a函數(shù)進(jìn)去,它是在watch里面運(yùn)行的,這樣就會(huì)收集依賴。當(dāng)使用reactiveapi的時(shí)候就要傳一個(gè)這樣函數(shù)進(jìn)去,當(dāng)使用refapi的時(shí)候,watch中第一個(gè)參數(shù)可以寫成count,因?yàn)閏ount是一個(gè)對(duì)象,如下:
舉個(gè)栗子??:?直接傳count
import { reactive, ref, watch } from"vue";
const state = reactive({a: 1,b: 2,});
const count = ref(0);
watch(count,(newValue,oldValue) =>{
console.log("新值",newValue,"舊值",oldValue);
})
count.value++;//修改依賴
效果展示:

如果說(shuō)傳了一個(gè)count.value進(jìn)去,那么也會(huì)報(bào)錯(cuò),因?yàn)閏ount.value拿到的也是屬性值了,如下:
舉個(gè)栗子??:?直接傳count
import { reactive, ref, watch } from"vue";
const state = reactive({a: 1,b: 2,});
const count = ref(0);
watch(count.value,(newValue,oldValue) =>{
console.log("新值",newValue,"舊值",oldValue);
})
count.value++;//修改依賴
效果展示:

從運(yùn)行結(jié)果看,也報(bào)了一個(gè)警告,所以說(shuō)這塊得注意下??
對(duì)了,差點(diǎn)忘了,watch是可以監(jiān)控多個(gè)數(shù)據(jù)的,如下:
舉個(gè)栗子??:?傳count和()=>state.a
import { reactive, ref, watch } from"vue";
const state = reactive({a: 1,b: 2,});
const count = ref(0);
watch([()=>state.a,count],([newValue1,newValue2],[oldValue1,oldValue2]) =>{
console.log("新值",newValue1,newValue2,"舊值",oldValue1,oldValue2);
})
count.value++;
state.a++;
效果展示:

?? 注意:無(wú)論是watch還是watchEffect,當(dāng)依賴變化時(shí),回調(diào)函數(shù)都是異步執(zhí)行的,當(dāng)然也會(huì)到微隊(duì)列等待執(zhí)行。
總得來(lái)說(shuō)watchEffect是最方便的,因?yàn)樗鼤?huì)自動(dòng)跟蹤依賴的變化,不需要手動(dòng)指定,但是有時(shí)候卻不得不使用watch,比如說(shuō):我們不希望回調(diào)函數(shù)一開始就執(zhí)行,只想讓它當(dāng)數(shù)據(jù)改變的時(shí)候才執(zhí)行,這時(shí)候就只能用watch了,還有一種就是數(shù)據(jù)改變時(shí),我們需要知道舊值是什么?這時(shí)也需要使用watch,最后一種就是我們需要監(jiān)控一些回調(diào)函數(shù)中用不到的數(shù)據(jù),比方說(shuō),輸出console.log("我要變了"),這在watchEffect中是做不到的
?? 判斷
在獲取響應(yīng)式數(shù)據(jù)的時(shí)候有四種api,分別是reactive、readonly、ref、computed,返回有兩種形式,一個(gè)是對(duì)象代理,另一個(gè)是{value:...}這種形式,有時(shí)候我們可能沒(méi)睡醒,大腦混亂,于是vue3提供了4種api用于區(qū)分到底是哪種方式獲取的響應(yīng)式數(shù)據(jù):
isProxy : 用于判斷某個(gè)數(shù)據(jù)是否是由
reactive或readonly獲取的isReative : 判斷某個(gè)數(shù)據(jù)是否是通過(guò)
reative創(chuàng)建的,具體可以看這個(gè)鏈接:?v3.vuejs.org/api/basic-r…[1]isReadonly :判斷某個(gè)數(shù)據(jù)是否是通過(guò)
readonly創(chuàng)建的isRef :判斷某個(gè)數(shù)據(jù)是否是一個(gè)
ref對(duì)象
?? 轉(zhuǎn)換
有時(shí)候我們拿到一個(gè)數(shù)據(jù),我們也不知道它是個(gè)啥,可能有時(shí)候也顧不上,到底是ref呢,還是proxy,鬼都不知道它是個(gè)啥!
這時(shí)我們可以用unref
- unref
unref?等同于:isRef(val) ? val.value : val,如果是ref的話那就把val.value值拿出來(lái),如果不是就拿val值
舉個(gè)栗子??:
function useNewTodo(todos){
todos = unref(todos);
//...其它代碼
}
- toRef
toRef會(huì)把一個(gè)響應(yīng)式對(duì)象中的某個(gè)屬性變成ref格式的數(shù)據(jù)
舉個(gè)栗子??:
import { reactive, toRef } from"vue";
const state = reactive({ name: "法醫(yī)", age: 18 });
const nameRef = toRef(state,"name");
console.log(nameRef);//最終是 {value:...}這種形式
nameRef.value = "前端獵手";//當(dāng)修改這個(gè)值后,state里面的數(shù)據(jù)也會(huì)受到影響
console.log(state.name);
效果展示:

- toRefs
把一個(gè)響應(yīng)式對(duì)象的所有屬性轉(zhuǎn)換為ref格式,然后包裝到plain-object普通對(duì)象中返回
舉個(gè)栗子??:
import { reactive, toRefs } from"vue";
const state = reactive({ name: "法醫(yī)", age: 18 });
const stateAsRefs = toRefs(state);
console.log(stateAsRefs);
/*
stateAsRefs 它不是一個(gè)代理對(duì)象了,而是一個(gè)普通對(duì)象,如下格式:
{
name:{value:ObjectRefImpl},
age:{value:ObjectRefImpl}
}
*/
效果展示:

為什么要這么做?
舉個(gè)栗子??:?我們需要把兩個(gè)響應(yīng)式數(shù)據(jù)混合在一起,如果直接使用展開運(yùn)算符那么就完蛋了,數(shù)據(jù)會(huì)失去響應(yīng)式
setup() {
const state1 = reactive({a:1,b:2});
const state2 = reactive({c:3,d:4});
return {
...state1,// 失去響應(yīng)式,相當(dāng)于在這寫了一個(gè)a:1,b:2
...state2,// 失去響應(yīng)式,相當(dāng)于在這寫了一個(gè)c:3,d:4
};
},
那么如何解決呢?外面套一個(gè)toRefs就好了
setup() {
const state1 = reactive({a:1,b:2});
const state2 = reactive({c:3,d:4});
return {
...toRefs(state1),// 具有響應(yīng)式
...toRefs(state2),// 具有響應(yīng)式
};
},
參考資料
[1]https://v3.vuejs.org/api/basic-reactivity.html#isreactive:?https://link.juejin.cn?target=https%3A%2F%2Fv3.vuejs.org%2Fapi%2Fbasic-reactivity.html%23isreactive
最后
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「?sherlocked_93?」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。
