全面解析Vue3 Reactive家族和Ref家族API

前言

你是不是習(xí)慣了Vue2的賦值即響應(yīng)式?Vue2還有個(gè)Vue.observable但你從沒(méi)用過(guò)?結(jié)果Vue3像跳跳糖一樣跳出來(lái)這么多的響應(yīng)式API,你有沒(méi)有懵逼的感覺(jué)?

不慌,挨個(gè)學(xué)。由于官方文檔寫的晦澀難懂,所以我寫下這篇。原創(chuàng):簡(jiǎn)書microkof。

首先說(shuō)明,全文提到的“基本數(shù)據(jù)”是指“數(shù)據(jù)類型為基本數(shù)據(jù)類型的數(shù)據(jù)”,“原始數(shù)據(jù)”是指被轉(zhuǎn)變?yōu)轫憫?yīng)式數(shù)據(jù)之前的純對(duì)象或基本數(shù)據(jù)。

reactive

Vue 3的根基。返回對(duì)象的響應(yīng)式副本,響應(yīng)式轉(zhuǎn)換是“深層”的——它影響所有嵌套property。返回Proxy對(duì)象,等于原始對(duì)象。建議只操作Proxy對(duì)象,不要操作原始對(duì)象。

官方建議,對(duì)來(lái)自于服務(wù)器的數(shù)據(jù)或者注定要響應(yīng)式的數(shù)據(jù)執(zhí)行reactive之前,最好不要用臨時(shí)變量?jī)?chǔ)存原始數(shù)據(jù),因?yàn)闆](méi)有意義,而且兩個(gè)變量容易讓初學(xué)者引起誤操作。

<template>
  <button @click="r.a++">count is: {{ r.a }}</button>
</template>

<script>
import { reactive } from "vue";
export default {
  setup() {
    let r = reactive({ a: 1 });
    return {
      r,
    };
  },
};
</script>

ref

ref說(shuō)白了就是reactive({value: 原始數(shù)據(jù)})。下方代碼如果打印r對(duì)象,會(huì)得到RefImpl(ref)對(duì)象,它有一個(gè)value屬性指向基礎(chǔ)類型值30。

<template>
  <button @click="r++">count is: {{ r }}</button>
</template>

<script>
import { ref } from "vue";
export default {
  setup() {
    let r = ref(30);
    return {
      r,
    };
  },
};
</script>

為什么似乎Proxy已經(jīng)解決所有問(wèn)題,還要有ref API呢?

因?yàn)镋S的Proxy API是為引用數(shù)據(jù)類型服務(wù)的,它無(wú)法為基本數(shù)據(jù)類型提供代理。如果強(qiáng)行代理,Vue會(huì)有提示:value cannot be made reactive: 30。

那么為什么Vue2的defineproperty并沒(méi)有區(qū)分基本數(shù)據(jù)類型和引用數(shù)據(jù)類型呢?

因?yàn)閐efineproperty就是Object的靜態(tài)方法,它只是為對(duì)象服務(wù)的,甚至無(wú)法對(duì)數(shù)組服務(wù),因此Vue 2弄了一個(gè)data根對(duì)象來(lái)存放基本數(shù)據(jù)類型,這樣無(wú)論什么類型,都是根對(duì)象的property,所以也就能代理基本數(shù)據(jù)類型。而Proxy能對(duì)所有引用類型代理,Vue 3也不再用data根對(duì)象,而是一個(gè)個(gè)的變量,所以帶來(lái)了新問(wèn)題,如何代理基本數(shù)據(jù)類型呢?并沒(méi)有原生辦法,只能構(gòu)建一個(gè){value: Proxy Object}結(jié)構(gòu)的對(duì)象,這樣Proxy也就能代理了。

問(wèn)題來(lái)了,同樣是響應(yīng)式結(jié)構(gòu),ref跟reactive的區(qū)別是什么?

ref與reactive的區(qū)別

對(duì)比 ref reactive
返回?cái)?shù)據(jù)類型 RefImpl對(duì)象(也叫ref對(duì)象) Proxy對(duì)象
傳入基本類型返回 {value: 基本類型} 禁止這么做
傳入引用類型返回 {value: Proxy對(duì)象} Proxy對(duì)象

兩者分別適用場(chǎng)合:

  1. ref可以為基本類型添加響應(yīng)式,也可以為引用類型添加響應(yīng)式,reactive只能為引用類型添加響應(yīng)式。

  2. 對(duì)于引用類型,什么時(shí)候用ref,什么時(shí)候用reactive?簡(jiǎn)單說(shuō),如果你只打算修改引用類型的一個(gè)屬性,那么推薦用reactive,如果你打算變量重賦值,那么一定要用ref。具體見下文。

使用組合式API的話,請(qǐng)一定了解“重賦值自身”和“重賦值自身屬性”的區(qū)別

這一點(diǎn)非常重要。先看配置項(xiàng)式API中重賦值Proxy的范例,跟Vue 2沒(méi)有區(qū)別,頁(yè)面會(huì)渲染出[ { "name": "趙六", "age": 21 } ]字符:

<template>
  <div>
    {{ jsonData }}
  </div>
</template>

<script>
export default {
  data() {
    return {
      jsonData: [
        {
          name: "牛二",
          age: 13,
        },
      ],
    };
  },
  created() {
    this.jsonData = [
      {
        name: "王五",
        age: 19,
      },
    ];
  },
  mounted() {
    this.jsonData = [
      {
        name: "趙六",
        age: 21,
      },
    ];
  },
};
</script>

而使用組合式API,你會(huì)發(fā)現(xiàn),reactive后的Proxy在onMounted中重賦值無(wú)法觸發(fā)渲染,最終頁(yè)面顯示[ { "name": "王五", "age": 19 } ]而不是趙六:

<template>
  <div>
    {{ jsonData }}
  </div>
</template>

<script>
import { onMounted, reactive } from "vue";
export default {
  setup() {
    let jsonData = reactive([
      {
        name: "牛二",
        age: 13,
      },
    ]);
    jsonData = reactive([
      {
        name: "王五",
        age: 19,
      },
    ]);
    onMounted(() => {
      jsonData = reactive([
        {
          name: "趙六",
          age: 100,
        },
      ]);
    });
    return {
      jsonData,
    };
  },
};
</script>

原因在于jsonData盡管是響應(yīng)式的,但是響應(yīng)式的是它的屬性,而不是它自身,重賦值它自身跟重賦值它的屬性是兩碼事。所以,想在組合式API中讓數(shù)據(jù)具備響應(yīng)式,必須用ref,因?yàn)?code>ref又對(duì)Proxy包裝了一層,修改ref其實(shí)是修改它的value,它的value一定是響應(yīng)式的,因此視圖就正常更新了。

再多說(shuō)一點(diǎn),如果數(shù)據(jù)是服務(wù)器返回的LIST數(shù)據(jù),而且只顯示、不變更,那么最好是使用shallowRef來(lái)包裝數(shù)據(jù),可以節(jié)能。如果會(huì)有變更,那么應(yīng)該用ref。

下例中采用了Ref語(yǔ)法糖。頁(yè)面會(huì)顯示Michael的信息:

<template>
  <div>
    {{ jsonData }}
    <ul>
      <li v-for="item in jsonData" :key="item.name">
        {{ item.name }} - {{ item.age }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { onMounted, ref } from "vue";
ref: jsonData = [
  {
    name: "Jim",
    age: 13,
  },
];
jsonData = [
  {
    name: "Tom",
    age: 19,
  },
];
onMounted(() => {
  jsonData = [
    {
      name: "Michael",
      age: 100,
    },
  ];
});
</script>

現(xiàn)在好像ref把引用數(shù)據(jù)類型也管起來(lái)了,到底啥時(shí)候才適合用reactive呢?很簡(jiǎn)單啊,如果你確信你只可能去改引用類型數(shù)據(jù)的屬性,那么一定要用reactive,如果還有可能要整體重賦值,那還得用ref。所以說(shuō):需要在組合式API里給變量重賦值的話,無(wú)論什么數(shù)據(jù)類型都必須用ref,不可以用reactive。

到此,我們清楚了ref與reactive都必須存在的理由,接著說(shuō),reactive有一套兄弟API,ref也有一套,它們都是干什么用的?先看reactive的:

reactive與shallowReactive的區(qū)別

打印的話,乍一看沒(méi)有區(qū)別,但是,shallow的中文意義是“淺層的”,shallowReactive不代理深層property,只會(huì)指向原始對(duì)象的深層property。

注意,給shallowReactive傳入Proxy是沒(méi)有意義的,即便這么做,直接返回該P(yáng)roxy。

shallowReactive的用途是:如果一個(gè)對(duì)象的深層不可能變化,那么就沒(méi)必要深層響應(yīng),這時(shí)候用shallowReactive可以節(jié)省系統(tǒng)開銷。

下例中,按下第2個(gè)button不會(huì)有反應(yīng),只有又去按下第1個(gè)button之后,視圖刷新,第二個(gè)button才有反應(yīng)。

<template>
<div>
  <button @click="r.b.c++">count is: {{ r.b.c }}</button>
  <button @click="s.b.c++">count is: {{ s.b.c }}</button>
</div>
</template>

<script>
import { reactive, shallowReactive } from "vue";
export default {
  setup() {
    let r = reactive({a: 1, b: {c: 2}});
    console.log(r);
    let s = shallowReactive({a: 1, b: {c: 2}});
    console.log(s);
    return {
      r,s
    };
  },
};
</script>

reactive與readonly的區(qū)別

reactive一般只接受ES普通的引用數(shù)據(jù)類型,盡管它也可以接受Proxy對(duì)象,但是沒(méi)有意義、沒(méi)有必要,但readonly可以接受Proxy對(duì)象,而且有實(shí)際意義,它可以獲取純對(duì)象或者Proxy或者RefImpl,返回原始代理的只讀代理。說(shuō)白了它做2步操作,先reactive,然后另生成一個(gè)只讀Proxy。

readonly的只讀是深層的只讀:訪問(wèn)的任何嵌套property也是只讀的。

readonly存在的意義有2個(gè),一個(gè)是保護(hù)數(shù)據(jù)不被修改,另一個(gè)是提升性能。

下例中,第2個(gè)button點(diǎn)擊不會(huì)有反應(yīng)。

<template>
<div>
  <button @click="r.b.c++">count is: {{ r.b.c }}</button>
  <button @click="s.b.c++">count is: {{ s.b.c }}</button>
</div>
</template>

<script>
import { reactive, readonly } from "vue";
export default {
  setup() {
    let r = reactive({a: 1, b: {c: 2}});
    console.log(r);
    let s = readonly({a: 1, b: {c: 2}});
    console.log(s);
    return {
      r,s
    };
  },
};
</script>

readonly與shallowReadonly的區(qū)別

就像reactive與shallowReactive的一樣,shallowReadonly只會(huì)給對(duì)象的第一層property設(shè)置只讀,不去管深層property,因此深層property并沒(méi)有被代理,只會(huì)指向原始對(duì)象。

下例中:

按下button1會(huì)有報(bào)錯(cuò)提示:Set operation on key "c" failed: target is readonly.,因?yàn)閞是深層只讀的。

按下button2沒(méi)有任何反應(yīng),因?yàn)閟hallowReadonly的深層是指向原始值的,修改原始對(duì)象不會(huì)反映到視圖上。

按下button3也會(huì)有報(bào)錯(cuò)提示:Set operation on key "a" failed: target is readonly.,因?yàn)閟hallowReadonly是淺層只讀的,a恰好是淺層property。

<template>
<div>
  <button @click="r.b.c++">count is: {{ r.b.c }}</button>
  <button @click="s.b.c++">count is: {{ s.b.c }}</button>
  <button @click="s.a++">count is: {{ s.a }}</button>
</div>
</template>

<script>
import { readonly, shallowReadonly } from "vue";
export default {
  setup() {
    let r = readonly({a: 1, b: {c: 2}});
    console.log(r);
    let s = shallowReadonly({a: 1, b: {c: 2}});
    console.log(s);
    return {
      r,s
    };
  },
};
</script>

shallowReactive與shallowReadonly的區(qū)別

首先說(shuō),兩者對(duì)深層property的態(tài)度是一致的,即“不去代理”,深層property都是指向原始對(duì)象的深層property,都允許直接修改原始對(duì)象的深層property,區(qū)別在于對(duì)待淺層property方面。

shallowReactive允許修改淺層property,shallowReadonly不允許,Vue3會(huì)阻止并給出報(bào)錯(cuò)。

isReactive、isReadonly、isProxy的區(qū)別

  • isReactive:Proxy是否是由reactive創(chuàng)建,是則返回true
  • isReadonly:Proxy由readonly創(chuàng)建則返回true
  • isProxy:上面兩個(gè)滿足任意一條,就返回true

上例中,如果加入這3條打印會(huì)得到什么?

    console.log(isReadonly(s)); // true
    console.log(isReadonly(s.a)); // false,因?yàn)閟.a得到的是a的值,而不是a自身,a的值當(dāng)然不是響應(yīng)式的
    console.log(isReadonly(s.b)); // false,道理同上

toRaw是什么

官方已經(jīng)解釋的很清楚,返回proxy的原始對(duì)象。這是一個(gè)轉(zhuǎn)義口,2個(gè)作用:可用于臨時(shí)讀取而不會(huì)引起proxy訪問(wèn)/跟蹤開銷,也可用于寫入而不會(huì)觸發(fā)視圖更新。

官方又說(shuō),不建議保留對(duì)原始對(duì)象的持久引用。請(qǐng)謹(jǐn)慎使用。這句話什么意思?就是說(shuō):

  1. 盡量不要把原始對(duì)象賦值給變量,盡量減少中間變量;

  2. 將原始對(duì)象轉(zhuǎn)換為Proxy之后,如果你臨時(shí)打算操作一下原始對(duì)象,那么也不要因?yàn)檫@個(gè)目的就早早的把原始對(duì)象賦值給變量,而是應(yīng)該用toRaw(proxy),以獲取原始對(duì)象,比如得到一個(gè)變量R,然后你可以操作R,操作完成之后就不要再碰R,而應(yīng)繼續(xù)操作Proxy。

下例中:

按下button1,會(huì)發(fā)現(xiàn)button2也跟著變,這表明Proxy的基本原理:操作Proxy會(huì)反映到原始對(duì)象身上。

按下button2,沒(méi)有任何反應(yīng),表明操作原始對(duì)象不會(huì)反映到視圖上。這時(shí)候重新按下button1,會(huì)發(fā)現(xiàn)數(shù)字跳躍了幾個(gè)數(shù),這表明直接修改原始對(duì)象之后,Proxy對(duì)原始對(duì)象繼續(xù)代理,并不需要重新reactive。

<template>
<div>
  <button @click="r.b.c++">count is: {{ r.b.c }}</button>
  <button @click="s.b.c++">count is: {{ s.b.c }}</button>
</div>
</template>

<script>
import { reactive, toRaw } from "vue";
export default {
  setup() {
    let r = reactive({a: 1, b: {c: 2}});
    console.log(r);
    let s = toRaw(r);
    console.log(s);
    return {
      r,s
    };
  },
};
</script>

markRaw與readonly的區(qū)別

markRaw是操作原始對(duì)象的,它的意義是將原始對(duì)象或者原始對(duì)象的某個(gè)淺層或深層property標(biāo)記為“永遠(yuǎn)不允許被代理”。Vue3會(huì)給對(duì)象的第一層或某深層加一個(gè)標(biāo)記__v_skip: true,這樣,即便原始對(duì)象被reactive之后,得到的該層和更深層就不會(huì)被代理。

如果想給原始對(duì)象的某個(gè)property加markRaw,需要執(zhí)行3步,先定義變量指向該property,然后markRaw這個(gè)變量得到新對(duì)象,然后讓源對(duì)象的property指向新對(duì)象。

如果將加了標(biāo)記的原始對(duì)象當(dāng)做其他原始對(duì)象的屬性,其他原始對(duì)象被reactive之后,加了標(biāo)記的對(duì)象依然不會(huì)被reactive。也就是說(shuō),reactive見了__v_skip: true就繞著走。

注意:雖然Vue3只會(huì)在表層加標(biāo)記,但是會(huì)影響深層的property。

markRaw與readonly的區(qū)別,在于側(cè)重點(diǎn)不同:

  • markRaw允許被修改,但不允許被代理。這里盡管說(shuō)允許修改,但是修改的意義不大,畢竟Vue的核心思想是響應(yīng)式,在添加響應(yīng)式之前修改意義不大。

  • readonly不允許被修改,但已經(jīng)被代理。

它們兩者相同點(diǎn)在于,從不同角度節(jié)省系統(tǒng)開銷。

markRaw的用途:

首先說(shuō),直接給某個(gè)對(duì)象全盤markRaw是沒(méi)有意義的,因?yàn)槟憔褪情_發(fā)者,你不想讓某對(duì)象被reactive,那么你不去寫reactive就好了啊。所以markRaw的用途應(yīng)該是:允許對(duì)象被reactive,但是阻止對(duì)象的部分內(nèi)容被reactive。

markRaw與shallowReactive的區(qū)別

markRaw shallowReactive
作用 阻止響應(yīng)式 讓淺層property響應(yīng)式,不操作深層property
淺層property 阻止響應(yīng)式 執(zhí)行響應(yīng)式
深層property 阻止響應(yīng)式 不執(zhí)行響應(yīng)式,也不阻止
  • 如果希望阻止其他程序員將對(duì)象響應(yīng)式,則可以使用markRaw來(lái)保護(hù)。

  • 如果剛好打算不讓從第2層到最深層的所有property響應(yīng)式,那么用shallowReactive可能更好。

  • 如果想要更定制化的阻止某些property被響應(yīng)式,那么應(yīng)當(dāng)使用markRaw。例如下例中,x.b被標(biāo)記,然后重復(fù)值給自己,此時(shí)再將x響應(yīng)式,x.b依然沒(méi)有被響應(yīng)式。所以點(diǎn)擊button不會(huì)有反應(yīng)。

<template>
<div>
  <button @click="s.b.c++">count is: {{ s.b.c }}</button>
</div>
</template>

<script>
import { markRaw, reactive } from "vue";
export default {
  setup() {
    let x = {a:1, b: {c: 2}};
    x.b = markRaw(x.b);
    let s = reactive(x);
    console.log(isReactive(s)); // true
    console.log(isReactive(s.b)); // false
    return {
      s
    };
  },
};
</script>

最后我們看ref和它的兄弟API。

ref與shallowRef的區(qū)別

ref shallowRef
本質(zhì) reactive({value: 原始數(shù)據(jù)}) shallowReactive({value: 原始數(shù)據(jù)})
區(qū)別點(diǎn) {value: 原始數(shù)據(jù)}被深層響應(yīng)式 只有value被響應(yīng)式,原始數(shù)據(jù)沒(méi)有響應(yīng)式
傳入基本類型 兩個(gè)API無(wú)差別 兩個(gè)API無(wú)差別,性能考慮盡量用shallowRef
傳入引用類型 value指向Proxy value指向原始數(shù)據(jù)

shallowRef的作用是只對(duì)value添加響應(yīng)式,因此,必須是value被重新賦值才會(huì)觸發(fā)響應(yīng)式。shallowRef的出現(xiàn)主要是為了節(jié)省系統(tǒng)開銷。

下例中,點(diǎn)擊button1會(huì)有反應(yīng),點(diǎn)擊button2不會(huì)有反應(yīng)。關(guān)鍵是點(diǎn)擊button3,我們知道在<template>里,如果給s重新賦值,其實(shí)相當(dāng)于給s.value重新賦值,由于value是響應(yīng)式的,這時(shí)候button2和button3都會(huì)有變化。

<template>
  <div>
    <button @click="r.b.c++">count is: {{ r.b.c }}</button>
    <button @click="s.b.c++">count is: {{ s.b.c }}</button>
    <button @click="s = { a: 10, b: { c: 20 } }">count is: {{ s.b.c }}</button>
  </div>
</template>

<script>
import { ref, shallowRef } from 'vue';
export default {
  setup() {
    let r = ref({ a: 1, b: { c: 2 } });
    let s = shallowRef({ a: 1, b: { c: 2 } });
    return {
      r,
      s,
    };
  },
};
</script>

toRef是咋回事

先看看這個(gè)題目,看看Proxy對(duì)象里面的基本數(shù)據(jù)是否具備響應(yīng)式:

<template>
<div>
  <button @click="r.a++">count is: {{ r.a }}</button>
  <button>count is: {{ s }}</button>
</div>
</template>

<script>
import { reactive, toRef } from "vue";
export default {
  setup() {
    let r = reactive({a:1});
    console.log(r);
    let s = r.a;
    console.log(s);
    return {
      r,s
    };
  },
};
</script>

當(dāng)我點(diǎn)擊button1的時(shí)候,你說(shuō)button2會(huì)變嗎?并不會(huì)。變量s就是個(gè)基本數(shù)據(jù),沒(méi)有任何響應(yīng)式。很不爽是不是?現(xiàn)在我改改,把let s = r.a;改成let s = toRef(r, 'a');,然后再試試?

可以看到button2的數(shù)字跟著變了!這就是toRef的作用:當(dāng)一個(gè)變量指向一個(gè)對(duì)象的某個(gè)property,且這個(gè)property是基本數(shù)據(jù)類型時(shí),必須用toRef才能變量與對(duì)象的響應(yīng)式連接。如果這個(gè)property是引用數(shù)據(jù)類型,就不需要?jiǎng)佑胻oRef。

toRef的用途之一是用于傳參,可傳遞一個(gè)響應(yīng)式的基本數(shù)據(jù)類型。

toRef還有一個(gè)特點(diǎn)是可以提前綁定,看個(gè)例子,r的原始數(shù)據(jù)并沒(méi)有property叫c,但是我就任性,我就提前讓s賦值為toRef(r, 'c'),這時(shí)候兩個(gè)button上是沒(méi)有數(shù)據(jù)的,畢竟property c是不存在的,在我點(diǎn)擊button1之后,兩個(gè)button都顯示了3,說(shuō)明提前綁定是有用的。

<template>
<div>
  <button @click="r.c = 3">count is: {{ r.c }}</button>
  <button>count is: {{ s }}</button>
</div>
</template>

<script>
import { reactive, toRef } from "vue";
export default {
  setup() {
    let r = reactive({a:{b:2}});
    console.log(r);
    let s = toRef(r, 'c');
    console.log(s);
    return {
      r,s
    };
  },
};
</script>

ref與toRef的區(qū)別

ref toRef
用法 ref(原始值) toRef(Proxy, 'xxprop')
返回 ref對(duì)象 同左
誤區(qū) 不要給ref傳入純對(duì)象的屬性,毫無(wú)意義且造成困惑,應(yīng)傳原始值 不要給toRef傳入原始值,毫無(wú)意義且造成困惑,應(yīng)傳Proxy

講解一下誤區(qū)。比如下例中:

  • 點(diǎn)擊button1,打印的ref對(duì)象是如期待的{ count: 4 },視圖也更新為4,但是原始值并沒(méi)有變,依然是{ count: 3 }。這說(shuō)明:給ref傳純對(duì)象的屬性會(huì)造成困惑。

  • 刷新頁(yè)面,只點(diǎn)擊button2,打印的ref對(duì)象變了,原始值也變了,但是視圖沒(méi)有更新,還是3。說(shuō)明:給toRef傳入原始值是錯(cuò)誤的操作,應(yīng)當(dāng)傳入Proxy,但也證明toRef對(duì)傳入值是指向關(guān)系。

  • 刷新頁(yè)面,只點(diǎn)擊button3,一切如期待,說(shuō)明:上面的說(shuō)法是正確的。

<template>
  <div>
    <p>{{ state1 }}</p>
    <button @click="add1">增加</button>

    <p>{{ state2 }}</p>
    <button @click="add2">增加</button>

    <p>{{ state3.a }} - {{ state4 }}</p>
    <button @click="add3">增加</button>
  </div>
</template>

<script>
import { reactive, ref, toRef } from "vue";
export default {
  setup() {
    const obj = { count: 3 };
    const state1 = ref(obj.count);
    const state2 = toRef(obj, "count");
    const state3 = reactive({ a: 5 });
    const state4 = toRef(state3, "a");

    function add1() {
      state1.value++;
      console.log("原始值obj:", obj);
      console.log("state1:", state1);
    }

    function add2() {
      state2.value++;
      console.log("原始值obj:", obj);
      console.log("state2:", state2);
    }

    function add3() {
      state4.value++;
      console.log("state3:", state3);
      console.log("state4:", state4);
    }

    return { state1, state2, state3, state4, add1, add2, add3 };
  },
};
</script>

toRef與toRefs的區(qū)別

toRefs可以看做批量版本的toRef。

toRef toRefs
用法 toRef(Proxy, 'xxprop') toRefs(Proxy)
返回 ObjectRefImpl對(duì)象 同左
作用 創(chuàng)建變量到Proxy屬性的響應(yīng)式連接 創(chuàng)建變量每個(gè)屬性到Proxy每個(gè)屬性的響應(yīng)式連接
連接關(guān)系 一對(duì)一 多對(duì)多

下例中,當(dāng)點(diǎn)擊button1時(shí),所有button都會(huì)有反應(yīng)。

<template>
<div>
  <button @click="r.c = 3">count is: {{ r.c }}</button>
  <button>count is: {{ s }}</button>
  <button>count is: {{ t.c.value }}</button>
</div>
</template>

<script>
import { reactive, toRef, toRefs } from "vue";
export default {
  setup() {
    let r = reactive({a:{b:2}, c: 4});
    console.log(r);
    let s = toRef(r, 'c');
    console.log(s);
    let t = toRefs(r);
    console.log(t.c)
    return {
      r,s,t
    };
  },
};
</script>

toRefs的一大用途是變相解構(gòu)Proxy。首先了解一個(gè)常識(shí),Proxy如果解構(gòu),基本數(shù)據(jù)會(huì)丟失響應(yīng)式?,F(xiàn)在我既想要解構(gòu)Proxy,又不想丟失響應(yīng)式,怎么辦?可以使用toRefs。

下例中,變量c是基本數(shù)據(jù),它不具備響應(yīng)式,因此button1被點(diǎn)擊之后,button2不會(huì)跟著變。如果將let {c} = r;改成let {c} = toRefs(r);,則變量c具備了響應(yīng)式,button2會(huì)跟著變。

<template>
<div>
  <button @click="r.c = 3">count is: {{ r.c }}</button>
  <button>count is: {{ c }}</button>
</div>
</template>

<script>
import { reactive, toRefs } from "vue";
export default {
  setup() {
    let r = reactive({a:{b:2}, c: 4});
    let {c} = r;
    return {
      r,c
    };
  },
};
</script>

toRefs的簡(jiǎn)潔用法:

return {...toRefs(Proxy), others}可用于返回解構(gòu)的Proxy,而不需要?jiǎng)?chuàng)建一個(gè)臨時(shí)變量。

如果組件只需要返回一個(gè)解構(gòu)的Proxy,可以更簡(jiǎn)略:return toRefs(Proxy)

customRef是什么

customRef跟ref、toRef、toRefs有很大區(qū)別,它生成的ref對(duì)象會(huì)自定義get和set。

customRef的主要用途至少有2個(gè):

  • 時(shí)機(jī)上說(shuō),可以控制視圖更新的時(shí)機(jī),可以延遲更新。其他ref兄弟API都做不到。
  • 內(nèi)容上說(shuō),可以修改傳入的原始數(shù)據(jù),讓原始數(shù)據(jù)與返回值不相同。其他兄弟API也做不到。
<template>
<div>
{{text}} - <input v-model="text" />
</div>
</template>

<script>
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue + 1
          console.log(value)
          trigger()
        }, delay)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}
</script>

測(cè)試:在<input>里快速敲入一串字符,左邊的{{text}}位置會(huì)延遲出現(xiàn)結(jié)果,而且會(huì)節(jié)流,而且console.log(value)也會(huì)延遲打印。

解釋:track和trigger,其中track用于追蹤,寫在return之前。trigger是觸發(fā),用在賦值給value語(yǔ)句之后。

將ref改寫成customRef應(yīng)該怎么寫?去掉延時(shí),且value = newValue即可。

unref是什么

unref類似于toRaw。unref的本質(zhì)就是解包,把{value: Proxy || 基本數(shù)據(jù)}解成Proxy || 基本數(shù)據(jù)。unref對(duì)toRefs創(chuàng)造的對(duì)象的各個(gè)屬性也起作用,因?yàn)楦鱾€(gè)屬性也是ref對(duì)象。

由于Vue3的開發(fā)原則是盡量不要直接修改內(nèi)部值,對(duì)ref來(lái)講就是盡量不要修改Proxy,如果某些場(chǎng)景下非要直接修改Proxy,需要用unref臨時(shí)將ref還原為Proxy。與toRaw一樣,修改完P(guān)roxy之后并不需要重新執(zhí)行ref。

下例中,點(diǎn)擊button1和button2,兩個(gè)按鈕都會(huì)有反應(yīng)。

<template>
<div>
  <button @click="r.b.c++">count is: {{ r.b.c }}</button>
  <button @click="s.b.c++">count is: {{ s.b.c }}</button>
</div>
</template>

<script>
import { ref, unref } from "vue";
export default {
  setup() {
    let r = ref({a: 1, b: {c: 2}});
    let s = unref(r);
    return {
      r,s
    };
  },
};
</script>

toRaw與unref的區(qū)別

用這三行代碼就很容易說(shuō)明了:

    const obj = { count: 3 };
    const state = ref(obj);
    console.log(toRaw(state.value) === toRaw(unref(state))); // true

說(shuō)明:

  1. 當(dāng)變量state為ref對(duì)象時(shí),state.value === unref(state)為真,兩邊都是Proxy對(duì)象,也說(shuō)明,想得到ref的Proxy,有2種方式:.value或者unref,完全等價(jià)。

  2. 當(dāng)變量state為ref對(duì)象時(shí),toRaw(state.value) === toRaw(unref(state))兩邊都是原始值,也說(shuō)明想獲得ref對(duì)象的原始值且原始值為引用類型時(shí),有2種方式,toRaw(state.value)或者toRaw(unref(state)),如果原始值為基本類型,也有2種方式,state.value或者unref(state)。所以,toRaw并不是對(duì)ref沒(méi)用,ref想得到原始對(duì)象就得用toRaw。

  3. toRaw對(duì)ref對(duì)象無(wú)效,必須作用于ref.value。

triggerRef是什么

我們知道shallowRef返回的ref對(duì)象的value指向的是內(nèi)部值,如果,我既想使用shallowRef生成ref對(duì)象(為了節(jié)省開銷),又想偶爾修改value指向的內(nèi)部值的某個(gè)property,又希望那個(gè)ref對(duì)象得到響應(yīng),我該怎么辦?這時(shí)候可以用triggerRef。

下例中,如果注釋掉triggerRef(r),那么點(diǎn)擊button不會(huì)看到任何反應(yīng),因?yàn)?code>{a:{b:2}, c: 4}都是非響應(yīng)式的。

<template>
<div>
  <button @click="onClick">count is: {{ r.c }}</button>
</div>
</template>

<script>
import { shallowRef, triggerRef } from "vue";
export default {
  setup() {
    let r = shallowRef({a:{b:2}, c: 4});
    function onClick() {
      r.value.c = 6;
      triggerRef(r);
    }
    return {
      r, onClick
    };
  },
};
</script>

isRef是什么

這個(gè)不說(shuō)了。

Vue 2的ref在Vue 3怎么用?

這算是一個(gè)附錄。Vue 2的ref在Vue 3依然存在,依然那樣標(biāo)記:

<div ref="elA">div元素</div>

但是在setup() {}里并不是Vue 2那么用,而是定義一個(gè)頂層變量,值很簡(jiǎn)單,一律寫成ref(null)即可,重要的是變量名,變量名必須與<div ref="elA">elA一致,然后onMounted里才能獲取、操作elA元素,elA.value就指向這個(gè)DOM元素,最后不要忘記return這個(gè)elA變量:

import { ref, onMounted } from 'vue'
export default {
  setup() {
      const elA = ref(null);

      // 在掛載后才能通過(guò) elA 獲取到目標(biāo)元素
      onMounted(() => {
        elA.value.innerHTML = '內(nèi)容被修改'
      })

      return {elA}
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容