• 技术文章 >web前端 >Vue.js

    全面盘点一下vue3中的ref、reactive

    青灯夜游青灯夜游2023-03-02 19:40:51转载221

    知道大家使用 Vue3 的时候有没有这样的疑惑,“ref、rective 都能创建一个响应式对象,我该如何选择?”,“为什么响应式对象解构之后就失去了响应式?应该如何处理?” 今天咱们就来全面盘点一下 ref、reactive,相信看完你一定会有不一样的收获,一起学起来吧!

    reactive()

    基本用法

    在 Vue3 中我们可以使用 reactive() 创建一个响应式对象或数组:

    import { reactive } from 'vue'
    
    const state = reactive({ count: 0 })

    这个响应式对象其实就是一个 Proxy, Vue 会在这个 Proxy 的属性被访问时收集副作用,属性被修改时触发副作用。

    要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。【相关推荐:vuejs视频教程web前端开发

    <script>
    import { reactive } from 'vue'
    
    export default {  setup() {    const state = reactive({ count: 0 })    return {      state    }  }}
    </script>
    
    <template>
      <div>{{ state.count }}</div>
    </template>

    当然,也可以使用 <script setup><script setup> 中顶层的导入和变量声明可以在模板中直接使用。

    <script setup>
    import { reactive } from 'vue'
    
    const state = reactive({ count: 0 })
    </script>
    
    <template>
      <div>{{ state.count }}</div>
    </template>

    响应式代理 vs 原始对象

    reactive() 返回的是一个原始对象的 Proxy,他们是不相等的:

    const raw = {}
    const proxy = reactive(raw)
    
    console.log(proxy === raw) // false

    原始对象在模板中也是可以使用的,但修改原始对象不会触发更新。因此,要使用 Vue 的响应式系统,就必须使用代理。

    <script setup>
    const state = { count: 0 }
    function add() {
      state.count++
    }
    </script>
    
    <template>
      <button @click="add">
        {{ state.count }} <!-- 当点击button时,始终显示为 0 -->
      </button>
    </template>

    为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

    const raw = {}
    const proxy1 = reactive(raw)
    const proxy2 = reactive(raw)
    
    console.log(proxy1 === proxy2) // true
    
    console.log(reactive(proxy1) === proxy1) // true

    这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:

    const raw = {}
    const proxy = reactive({ nested: raw })
    const nested = reactive(raw)
    
    console.log(proxy.nested === nested) // true

    shallowReactive()

    在 Vue 中,状态默认都是深层响应式的。但某些场景下,我们可能想创建一个 浅层响应式对象 ,让它仅在顶层具有响应性,这时候可以使用 shallowReactive()

    const state = shallowReactive({
      foo: 1,
      nested: {
        bar: 2
      }
    })
    
    // 状态自身的属性是响应式的
    state.foo++
    
    // 下层嵌套对象不是响应式的,不会按期望工作
    state.nested.bar++

    注意:浅层响应式对象应该只用于组件中的根级状态。避免将其嵌套在深层次的响应式对象中,因为其内部的属性具有不一致的响应行为,嵌套之后将很难理解和调试。

    reactive() 的局限性

    reactive() 虽然强大,但也有以下几条限制:

    为了解决以上几个限制,ref 闪耀登场了!

    ref()

    Vue 提供了一个 ref() 方法来允许我们创建使用任何值类型的响应式 ref 。

    基本用法

    ref() 将传入的参数包装为一个带有 value 属性的 ref 对象:

    import { ref } from 'vue'
    
    const count = ref(0)
    
    console.log(count) // { value: 0 }
    
    count.value++
    console.log(count.value) // 1

    和响应式对象的属性类似,ref 的 value 属性也是响应式的。同时,当值为对象类型时,Vue 会自动使用 reactive() 处理这个值。

    一个包含对象的 ref 可以响应式地替换整个对象:

    <script setup>
    import { ref } from 'vue'
    
    let state = ref({ count: 0 })
    function change() {
      // 这是响应式替换
      state.value = ref({ count: 1 })
    }
    </script>
    
    <template>
      <button @click="change">
        {{ state }} <!-- 当点击button时,显示为 { "count": 1 } -->
      </button>
    </template>

    ref 从一般对象上解构属性或将属性传递给函数时,不会丢失响应性:

    参考 前端进阶面试题详细解答

    const state = {
      count: ref(0)
    }
    // 解构之后,和 state.count 依然保持响应性连接
    const { count } = state
    // 会影响 state
    count.value++
    
    // 该函数接收一个 ref, 和传入的值保持响应性连接
    function callSomeFunction(count) {
      // 会影响 state
      count.value++
    }
    callSomeFunction(state.count)

    ref() 让我们能创建使用任何值类型的 ref 对象,并能够在不丢失响应性的前提下传递这些对象。这个功能非常重要,经常用于将逻辑提取到 组合式函数 中。

    // mouse.js
    export function useMouse() {
      const x = ref(0)
      const y = ref(0)
    
      // ...
      return { x, y }
    }
    <script setup>
    import { useMouse } from './mouse.js'
    // 可以解构而不会失去响应性
    const { x, y } = useMouse()
    </script>

    ref 的解包

    所谓解包就是获取到 ref 对象上 value 属性的值。常用的两种方法就是 .valueunref()unref() 是 Vue 提供的方法,如果参数是 ref ,则返回 value 属性的值,否则返回参数本身。

    ref 在模板中的解包

    当 ref 在模板中作为顶层属性被访问时,它们会被自动解包,不需要使用 .value 。下面是之前的例子,使用 ref() 代替:

    <script setup>
    import { ref } from 'vue'
    
    const count = ref(0)
    </script>
    
    <template>
      <div>
        {{ count }} <!-- 无需 .value -->
      </div>
    </template>

    还有一种情况,如果文本插值({{ }})计算的最终值是 ref ,也会被自动解包。下面的非顶层属性会被正确渲染出来。

    <script setup>
    import { ref } from 'vue'
    
    const object = { foo: ref(1) }
    
    </script>
    
    <template>
      <div>
        {{ object.foo }} <!-- 无需 .value -->
      </div>
    </template>

    其他情况则不会被自动解包,如:object.foo 不是顶层属性,文本插值({{ }})计算的最终值也不是 ref:

    const object = { foo: ref(1) }

    下面的内容将不会像预期的那样工作:

    <div>{{ object.foo + 1 }}</div>

    渲染的结果会是 [object Object]1,因为 object.foo 是一个 ref 对象。我们可以通过将 foo 改成顶层属性来解决这个问题:

    const object = { foo: ref(1) }
    const { foo } = object
    <div>{{ foo + 1 }}</div>

    现在结果就可以正确地渲染出来了。

    ref 在响应式对象中的解包

    当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样:

    const count = ref(0)
    const state = reactive({ count })
    
    console.log(state.count) // 0
    
    state.count = 1
    console.log(state.count) // 1

    只有当嵌套在一个深层响应式对象内时,才会发生解包。当 ref 作为 浅层响应式对象 的属性被访问时则不会解包:

    const count = ref(0)
    const state = shallowReactive({ count })
    
    console.log(state.count) // { value: 0 } 而不是 0

    如果将一个新的 ref 赋值给一个已经关联 ref 的属性,那么它会替换掉旧的 ref:

    const count = ref(1)
    const state = reactive({ count })
    
    const otherCount = ref(2)
    state.count = otherCount
    
    console.log(state.count) // 2
    // 此时 count 已经和 state.count 失去连接
    console.log(count.value) // 1

    ref 在数组和集合类型的解包

    跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。

    const books = reactive([ref('Vue 3 Guide')])
    // 这里需要 .value
    console.log(books[0].value)
    
    const map = reactive(new Map([['count', ref(0)]]))
    // 这里需要 .value
    console.log(map.get('count').value)

    toRef()

    toRef 是基于响应式对象上的一个属性,创建一个对应的 ref 的方法。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

    const state = reactive({
      foo: 1,
      bar: 2
    })
    
    const fooRef = toRef(state, 'foo')
    
    // 更改源属性会更新该 ref
    state.foo++
    console.log(fooRef.value) // 2
    
    // 更改该 ref 也会更新源属性
    fooRef.value++
    console.log(state.foo) // 3

    toRef() 在你想把一个 prop 的 ref 传递给一个组合式函数时会很有用:

    <script setup>
    import { toRef } from 'vue'
    
    const props = defineProps(/* ... */)
    
    // 将 `props.foo` 转换为 ref,然后传入一个组合式函数
    useSomeFeature(toRef(props, 'foo'))
    </script>

    toRef 与组件 props 结合使用时,关于禁止对 props 做出更改的限制依然有效。如果将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。在这种场景下,你可以考虑使用带有 getsetcomputed 替代。

    注意:即使源属性当前不存在,toRef() 也会返回一个可用的 ref。这让它在处理可选 props 的时候非常有用,相比之下 toRefs 就不会为可选 props 创建对应的 refs 。下面我们就来了解一下 toRefs

    toRefs()

    toRefs() 是将一个响应式对象上的所有属性都转为 ref ,然后再将这些 ref 组合为一个普通对象的方法。这个普通对象的每个属性和源对象的属性保持同步。

    const state = reactive({
      foo: 1,
      bar: 2
    })
    
    // 相当于
    // const stateAsRefs = {
    //   foo: toRef(state, 'foo'),
    //   bar: toRef(state, 'bar')
    // }
    const stateAsRefs = toRefs(state)
    
    state.foo++
    console.log(stateAsRefs.foo.value) // 2
    
    stateAsRefs.foo.value++
    console.log(state.foo) // 3

    从组合式函数中返回响应式对象时,toRefs 相当有用。它可以使我们解构返回的对象时,不失去响应性:

    // feature.js
    export function useFeature() {
      const state = reactive({
        foo: 1,
        bar: 2
      })
    
      // ...
      // 返回时将属性都转为 ref
      return toRefs(state)
    }
    <script setup>
    import { useFeature } from './feature.js'
    // 可以解构而不会失去响应性
    const { foo, bar } = useFeature()
    </script>

    toRefs 只会为源对象上已存在的属性创建 ref。如果要为还不存在的属性创建 ref,就要用到上面提到的 toRef

    以上就是 ref、reactive 的详细用法,不知道你有没有新的收获。接下来,我们来探讨一下响应式原理。

    响应式原理

    Vue2 的限制

    大家都知道 Vue2 中的响应式是采⽤ Object.defineProperty() , 通过 getter / setter 进行属性的拦截。这种方式对旧版本浏览器的支持更加友好,但它有众多缺点:

    Vue3 的响应式系统

    针对上述情况,Vue3 的响应式系统横空出世了!Vue3 使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref ,完美的解决了上述几条限制。下面的代码可以说明它们是如何工作的:

    function reactive(obj) {
      return new Proxy(obj, {
        get(target, key) {
          track(target, key)
          return target[key]
        },
        set(target, key, value) {
          target[key] = value
          trigger(target, key)
        }
      })
    }
    
    function ref(value) {
      const refObject = {
        get value() {
          track(refObject, 'value')
          return value
        },
        set value(newValue) {
          value = newValue
          trigger(refObject, 'value')
        }
      }
      return refObject
    }

    不难看出,当将一个响应性对象的属性解构为一个局部变量时,响应性就会“断开连接”。因为对局部变量的访问不会触发 get / set 代理捕获。

    我们回到响应式原理。在 track() 内部,我们会检查当前是否有正在运行的副作用。如果有,就会查找到存储了所有追踪了该属性的订阅者的 Set,然后将当前这个副作用作为新订阅者添加到该 Set 中。

    // activeEffect 会在一个副作用就要运行之前被设置
    let activeEffect
    
    function track(target, key) {
      if (activeEffect) {
        const effects = getSubscribersForProperty(target, key)
        effects.add(activeEffect)
      }
    }

    副作用订阅将被存储在一个全局的 WeakMap<target, Map<key, Set<effect>>> 数据结构中。如果在第一次追踪时没有找到对相应属性订阅的副作用集合,它将会在这里新建。这就是 getSubscribersForProperty() 函数所做的事。

    trigger() 之中,我们会再次查找到该属性的所有订阅副作用。这一次我们全部执行它们:

    function trigger(target, key) {
      const effects = getSubscribersForProperty(target, key)
      effects.forEach((effect) => effect())
    }

    这些副作用就是用来执行 diff 算法,从而更新页面的。

    这就是响应式系统的大致原理,Vue3 还做了编译器的优化,diff 算法的优化等等。不得不佩服尤大大,把 Vue 的响应式系统又提升了一个台阶!

    (学习视频分享:vuejs入门教程编程基础视频

    以上就是全面盘点一下vue3中的ref、reactive的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    专题推荐:前端 Vue.js
    上一篇:带你深入了解Vue.$nextTick(原理浅析) 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • 【整理推荐】最受欢迎的9个Vue UI库• 浅析怎么利用axios和vue实现简易天气查询• 一文浅析Vue中父子组件间传值问题• 【整理分享】8 个实用Vue开发技巧• 聊聊Vue开发小程序的技术原理• 深入浅析Vue2中的Diff算法
    1/1

    PHP中文网