• 使用Ref还是Reactive?


    我喜欢Vue 3的Composition API,它提供了两种方法来为Vue组件添加响应式状态:refreactive。当你使用ref时到处使用.value是很麻烦的,但当你用reactive创建的响应式对象进行重构时,也很容易丢失响应性。 在这篇文章中,我将阐释你如何来选择reactive以及ref

    一句话总结:默认情况下使用ref,当你需要对变量分组时使用reactive

    Vue3的响应式

    在我解释refreactive之前,你应该了解Vue3响应式系统的基本知识。

    如果你已经掌握了Vue3响应式系统是如何工作的,你可以跳过本小节。

    很不幸,JavaScript默认情况下并不是响应式的。让我们看看下面代码示例:

    let price = 10.0
    const quantity = 2
    
    const total = price * quantity
    console.log(total) // 20
    
    price = 20.0
    console.log(total) // ⚠️ total is still 20
    

    在响应式系统中,我们期望每当price或者quantity改变时,total就会被更新。但是JavaScript通常情况下并不会像预期的这样生效。

    你也许会嘀咕,为什么Vue需要响应式系统?答案很简单:Vue 组件的状态由响应式 JavaScript 对象组成。当你修改这些对象时,视图或者依赖的响应式对象就会更新。

    因此,Vue框架必须实现另一种机制来跟踪局部变量的读和写,它是通过拦截对象属性的读写来实现的。这样一来,Vue就可以跟踪一个响应式对象的属性访问以及更改。

    由于浏览器的限制,Vue 2专门使用getters/setters来拦截属性。Vue 3对响应式对象使用Proxy,对ref使用getters/setters。下面的伪代码展示了属性拦截的基本原理;它解释了核心概念,并忽略了许多细节和边缘情况:

    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)
        },
      })
    }
    

    proxygetset方法通常被称为代理陷阱。

    这里强烈建议阅读官方文档来查看有关Vue响应式系统的更多细节。

    reactive()

    现在,让我们来分析下,你如何使用Vue3的reactive()函数来声明一个响应式状态:

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

    该状态默认是深度响应式的。如果你修改了嵌套的数组或对象,这些更改都会被vue检测到:

    import { reactive } from 'vue'
    
    const state = reactive({
      count: 0,
      nested: { count: 0 },
    })
    
    watch(state, () => console.log(state))
    // "{ count: 0, nested: { count: 0 } }"
    
    const incrementNestedCount = () => {
      state.nested.count += 1
      // Triggers watcher -> "{ count: 0, nested: { count: 1 } }"
    }
    

    限制

    reactive()API有两个限制:

    第一个限制是,它只适用于对象类型,比如对象、数组和集合类型,如MapSet。它不适用于原始类型,比如stringnumberboolean

    第二个限制是,从reactive()返回的代理对象与原始对象是不一样的。用===操作符进行比较会返回false

    const plainJsObject = {}
    const proxy = reactive(plainJsObject)
    
    // proxy is NOT equal to the original plain JS object.
    console.log(proxy === plainJsObject) // false
    

    你必须始终保持对响应式对象的相同引用,否则,Vue无法跟踪对象的属性。如果你试图将一个响应式对象的属性解构为局部变量,你可能会遇到这个问题:

    const state = reactive({
      count: 0,
    })
    
    // ⚠️ count is now a local variable disconnected from state.count
    let { count } = state
    
    count += 1 // ⚠️ Does not affect original state
    

    幸运的是,你可以首先使用toRefs将对象的所有属性转换为响应式的,然后你可以解构对象而不丢失响应:

    let state = reactive({
      count: 0,
    })
    
    // count is a ref, maintaining reactivity
    const { count } = toRefs(state)
    

    如果你试图重新赋值reactive的值,也会发生类似的问题。如果你"替换"一个响应式对象,新的对象会覆盖对原始对象的引用,并且响应式连接会丢失:

    const state = reactive({
      count: 0,
    })
    
    watch(state, () => console.log(state), { deep: true })
    // "{ count: 0 }"
    
    // ⚠️ The above reference ({ count: 0 }) is no longer being tracked (reactivity connection is lost!)
    state = reactive({
      count: 10,
    })
    // ⚠️ The watcher doesn't fire
    

    如果我们传递一个属性到函数中,响应式连接也会丢失:

    const state = reactive({
      count: 0,
    })
    
    const useFoo = (count) => {
      // ⚠️ Here count is a plain number and the useFoo composable
      // cannot track changes to state.count
    }
    
    useFoo(state.count)
    

    ref()

    Vue提供了ref()函数来解决reactive()的限制。

    ref()并不局限于对象类型,而是可以容纳任何值类型:

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

    为了读写通过ref()创建的响应式变量,你需要通过.value属性来访问:

    const count = ref(0)
    const state = ref({ count: 0 })
    
    console.log(count) // { value: 0 }
    console.log(count.value) // 0
    
    count.value++
    console.log(count.value) // 1
    
    state.value.count = 1
    console.log(state.value) // { count: 1 }
    

    你可能会问自己,ref()如何能容纳原始类型,因为我们刚刚了解到Vue需要一个对象才能触发get/set代理陷阱。下面的伪代码展示了ref()背后的简化逻辑:

    function ref(value) {
      const refObject = {
        get value() {
          track(refObject, 'value')
          return value
        },
        set value(newValue) {
          value = newValue
          trigger(refObject, 'value')
        },
      }
      return refObject
    }
    

    当拥有对象类型时,ref自动用reactive()转换其.value

    ref({}) ~= ref(reactive({}))
    

    如果你想深入了解,可以在源码中查看ref()实现

    不幸的是,也不能对用ref()创建的响应式对象进行解构。这也会导致响应式丢失:

    import { ref } from 'vue'
    
    const count = ref(0)
    
    const countValue = count.value // ⚠️ disconnects reactivity
    const { value: countDestructured } = count // ⚠️ disconnects reactivity
    

    但是,如果将ref分组在一个普通的JavaScript对象中,就不会丢失响应式:

    const state = {
      count: ref(1),
      name: ref('Michael'),
    }
    
    const { count, name } = state // still reactive
    

    ref也可以被传递到函数中而不丢失响应式。

    const state = {
      count: ref(1),
      name: ref('Michael'),
    }
    
    const useFoo = (count) => {
      /**
       * The function receives a ref
       * It needs to access the value via .value but it
       * will retain the reactivity connection
       */
    }
    
    useFoo(state.count)
    

    这种能力相当重要,因为它在将逻辑提取到组合式函数中时经常被使用。 一个包含对象值的ref可以响应式地替换整个对象:

    const state = {
      count: 1,
      name: 'Michael',
    }
    
    // Still reactive
    state.value = {
      count: 2,
      name: 'Chris',
    }
    

    解包refs()

    在使用ref时到处使用.value可能很麻烦,但我们可以使用一些辅助函数。

    unref实用函数

    unref()是一个便捷的实用函数,在你的值可能是一个ref的情况下特别有用。在一个非ref上调用.value会抛出一个运行时错误,unref()在这种情况下就很有用:

    import { ref, unref } from 'vue'
    
    const count = ref(0)
    
    const unwrappedCount = unref(count)
    // same as isRef(count) ? count.value : count`
    

    如果unref()的参数是一个ref,就会返回其内部值。否则就返回参数本身。这是的val = isRef(val) ? val.value : val语法糖。

    模板解包

    当你在模板上调用ref时,Vue会自动使用unref()进行解包。这样,你永远不需要在模板中使用.value进行访问:

    
    
    <template>
      <span>
        
        {{ count }}
      span>
    template>
    

    只在ref是模板中的顶级属性时才生效。

    侦听器

    我们可以直接传递一个ref作为侦听器的依赖:

    import { watch, ref } from 'vue'
    
    const count = ref(0)
    
    // Vue automatically unwraps this ref for us
    watch(count, (newCount) => console.log(newCount))
    

    Volar

    如果你正在使用VS Code,你可以通过配置Volar扩展来自动地添加.valueref上。你可以在Volar: Auto Complete Refs设置中开启:

    image.png

    相应的JSON设置:

    "volar.autoCompleteRefs": true
    

    为了减少CPU的使用,这个功能默认是禁用的。

    比较

    让我们总结一下reactiveref之间的区别:

    reactive ref
    👎 只对对象类型起作用 👍对任何类型起作用
    👍在

    你可以简单地将data中的所有内容复制到reactive中,然后将这个组件迁移到Composition API中:

    
    

    比较ref和reactive

    一个推荐的模式是在一个reactive对象中对ref分组:

    const loading = ref(true)
    const error = ref(null)
    
    const state = reactive({
      loading,
      error,
    })
    
    // You can watch the reactive object...
    watchEffect(() => console.log(state.loading))
    
    // ...and the ref directly
    watch(loading, () => console.log('loading has changed'))
    
    setTimeout(() => {
      loading.value = false
      // Triggers both watchers
    }, 500)
    

    如果你不需要state对象本身的响应式,你可以在一个普通的JavaScript对象中进行分组。 对 refs 进行分组的结果是一个单一的对象,它更容易处理,并使你的代码保持有序。你可以看到分组后的 refs 属于一起,并且是相关的。

    这种模式也被用于像Vuelidate这样的库中,他们使用reactive()来设置验证的状态。

    总结起来,社区中的最佳实践是默认使用ref,在需要分组的时候使用reactive

    总结

    那么,你究竟该使用ref还是reactive

    我的建议是默认使用ref,当你需要分组时使用reactive。Vue社区也有同样的观点,但如果你决定默认使用reactive,也完全没有问题。

    refreactive都是在Vue 3中创建响应式变量的强大工具。你甚至可以在没有任何技术缺陷的情况下同时使用它们。只要你选择你喜欢的那一个,并尽量在写代码时保持一致就可以了!

    以上就是本文的全部内容,如果对你有所帮助,欢迎点赞、收藏、转发~

  • 相关阅读:
    【安全利器SELinux快速入门系列】SELinux基础入门
    MySQL 主从复制与读写分离
    ES可视化工具--ElasticHD--下载、安装、使用
    美容院圣诞节促销活动方案
    桌面软件开发框架大赏
    Docker部署单点Elasticsearch与Kibana
    java8 新特性5 方法引用
    Linux(Ubuntu)下源码开发整个流程完成版本(下载->编译->模拟器运行)
    大龄程序员谈架构经验 内行看门道
    NSA 和 CISA 联合揭露当下十大网络安全错误配置
  • 原文地址:https://www.cnblogs.com/chuckQu/p/17350975.html