Vue 3 之:弄清 ref reactive toRef toRefs

reactive

reactive 会对传入对象进行包裹,创建一个该对象的 Proxy 代理。它是源对象的响应式副本,不等于原始对象。它“深层”转换了源对象的所有嵌套 property,解包并维持其中的任何ref引用关系。
reactive API很好地解决了 Vue2 通过 defineProperty 实现数据响应式时的缺陷。使用也非常简单:

<template>
  <div>
    <!-- 4. 访问响应式数据对象中的 age  -->
    {{ state.age }}
  </div>
</template>

<script>
// 1. 从 vue 中导入 reactive 
import { reactive } from 'vue'
export default {
  name: 'User',
  setup() {
    // 2. 创建响应式的数据对象
    const user = reactive({ age: 33 })
    // 3. 将响应式数据对象 user return 出去,供 template 使用
    return { user }
  }
}
</script>

当将 ref 分配给 reactive property 时,ref 将被自动解包,无需再用.value访问。

import { ref, reactive } from 'vue'
export default {
  setup() {
    const num1 = ref(1)
    const num2 = ref(2)
    const obj = reactive({ num1 })

    // 把 ref 分配给 reactive property,ref 将被自动解包。
    obj.num2 = num2
    console.log(obj.num2) // 2
    console.log(obj.num2 === num2.value) // true

    // ref 会被解包
    console.log(obj.num1 === num1.value) // true

    // 修改 `num1` ref 的值会更新 `obj.num1`的值
    num1.value++
    console.log(num1.value) // 2
    console.log(obj.num1) // 2

    // 修改 obj 的 num1 property 也会更新 `num1` ref
    obj.num1++
    console.log(obj.num1) // 3
    console.log(num1.value) // 3

    console.log(obj) // Proxy {num1: RefImpl, num2: RefImpl}
  }
}

ref

ref 函数用来将一项数据包装成一个响应式 ref 对象。它接收任意数据类型的参数,作为这个 ref 对象 内部的 value property 的值。之后可以用ref对象.value访问或更改这个值。
因为基础数据类型只能传递值而不是引用地址,将它包装在一个对象内,可以实现数据的响应式。

可以通过 isRef 判断变量是否是 Ref 对象。
如果将对象分配为 ref 值,则内部会通过 reactive 方法使该对象具有高度的响应式。

<script>
import { ref } from 'vue'
export default {
  name: 'User',
  setup() {
    const user = ref({ age: 30 })
    const userJob = ref('前端工程师')
    console.log(user.value) // { age: 30 }
    console.log(userJob.value) // '前端工程师'
    console.log(user) // RefImpl {...}
  }
}
</script>

有时我们可能需要为 ref 的内部值指定复杂类型。想要简洁地做到这一点,我们可以在调用 ref 覆盖默认推断时传递一个泛型参数:

const foo = ref<string | number>('foo') // foo 的类型:Ref<string | number>
foo.value = 123 // ok!

因为ref就是通过reactive包装了一个对象 ,然后将值传给该对象的value属性,这也就是为什么每次访问时我们都需要加上.value。可以简单地把 ref(obj) 理解为 reactive({ value: obj })

如何选择 refreactive?建议:

  1. 基础类型值(StringNumberBoolean等) 或单值对象(类似{ count: 3 }这样只有一个属性值的对象) 使用 ref
  2. 引用类型值(ObjectArray)使用 reactive
  3. 对于 ref 对象可以使用 unref 语法糖来免去.value访问的困扰

toRef

toRef 函数可以为传入对象的某个属性新创建一个响应式引用 ref。这个 ref 可以被传递,它会保持对其源 property 的响应式连接。
第一个参数为源对象,第二个参数为源对象中的属性名。

const state = reactive({
  foo: 1,
})
const fooRef = toRef(state, 'foo') // 和 state 的 foo 属性建立了高度响应式连接

fooRef.value++
console.log(state.foo) // 2
// 原 Proxy 对象 state 的 foo 被影响了

state.foo++
console.log(fooRef.value) // 3
// 同步修改了 fooRef 的值

console.log(fooRef) // ObjectRefImpl {_object: Proxy, _key: "foo", __v_isRef: true}

再通过个小 🌰 对比下 reftoRef

<template>
  <p>ref state1: {{ state1.count }}</p>
  <button @click="add1">增加 state1 的 count</button>
  <p>toRef state2: {{ state2 }}</p>
  <button @click="add2">增加 state2</button>
</template>

<script>
import { ref, toRef } from 'vue'
export default {
  setup() {
    const obj = { count: 3 }
    const state1 = ref(obj) 
    // const state1 = ref(obj.count) // 如果传简单数据类型是值传递,无法体现对源数据的响应式
    const state2 = toRef(obj, 'count')
    function add1() {
      state1.value.count ++
      console.log('原始值:', obj); // obj的 count 递增
      console.log('响应式数据对象:', state1.value.count); 
      console.log('state2', state2.value)
      // state1 的 count 属性 和 state 2 递增,页面上的 state1 和 state2 也递增
    }
    function add2() {
      state2.value ++
      console.log('原始值:', obj); // obj 的 count 递增
      console.log('响应式数据对象:', state2.value); 
      console.log('state1', state1.value.count)
      // state2 的值 和 state1 的 count 属性也同步递增,页面上的 state1 和 state2无变化
    }

    return { state1, state2, add1, add2 }
  }
}
</script>

得出结论:
ref 创建一个响应式对象,如果传入参数是对象,那么与对象所有嵌套属性都维持数据响应。它的作用是 data 选项 般的存在,即组件内部状态ref值改变会触发页面渲染,同时能作为props或 事件参数 进行组件通信。
toRef 是对传入对象指定属性的响应式绑定值改变不会更新视图。因此用它创建的变量不作用于模版渲染,而是用来接收诸如props引用式传递

当你要将 prop 的某个ref(即用 ref 包装的属性) 传递给复合函数时,toRef 很有用:

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

即使源 property 不存在,toRef 也会返回一个可用的 ref。这使得它在使用可选 prop 时特别有用,因为可选 prop 并不会被 toRefs 处理。

toRefs

了解完 toRef 后,就很好理解 toRefs 了,其作用是生成一个新对象,内部每个属性都指向传入的对象的相应 property 的响应式数据 ref
也就是说,新对象本身与原对象的无关联(指向新的引用地址),但它的所有属性却都与源对象的对应属性建立了响应性。
toRef 可以记成建立一个 ref 属性值的引用,toRefs 则是所有 ref 响应属性值的引用。

看下 🌰:
如果对响应式对象进行解构,被解构的两个 property 的响应性都会丢失。

const book = reactive({
  author: 'Vue Team',
  year: '2020',
  title: 'Vue 3 Guide',
  description: 'You are reading this book right now ;)',
  price: 'free'
})

let { author, title } = book

可以用toRefs函数建立起与源对象的响应式关联:

const book = reactive({ ... })
let { author, title } = toRefs(book)

title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
book.author = 'Jay Chou'
console.log(author.value) // 'Jay Chou'

实际开发中,比如需要解构 props,就可以这样操作来保证与 props 参数的响应式引用:

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)
  console.log(title.value)
}

但如果 title 是可选 prop,则传入的 props 中可能没有 title 。这种情况下,toRefs 将不会为 title 创建一个 ref ,此时就需要用 toRef 替代它:

import { toRef } from 'vue'
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

像上面这样做确保我们的侦听器能够根据 title prop 的变化做出反应

当从组合式函数返回响应式对象时, toRefs非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // ... 操作 state 的逻辑

  // 返回时转换为ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以在不失去响应性的情况下解构
    const { foo, bar } = useFeatureX()
    return {
      foo,
      bar
    }
  }
}

总结

  • refreactive是在setup()声明组件内部状态用的, 这些变量通常都要 return 出去,除了供<template>或渲染函数渲染视图,也可以作为 propsemit 参数 在组件间传递。它们的值变更可触发页面渲染

  • toReftoRefs主要用于处理 组件/函数 传递的响应式数据,譬如在接收父组件props/composables组合式函数返回数据时 建立起某些属性的响应式引用

  • 通过ref包装的属性在 setup 函数内都需要通过.value去访问它值 (template 模版内不用)。因此,reftoRef创建的变量值都需要用变量.value读取。reactive则不用,因为会自动解包分配给它的ref
    至于toRefs,如果是解构赋值,如const { state1, state2 } = toRefs(props),值需要这样获取:state1.value.count
    若整体赋给一个变量,如const state = toRefs(props),则是state.state1.value

  • 只有toRefs可以解构。

  • 以上四种方式声明的变量在通过 props 或 事件 传递时,均会维持其响应性

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容