Vue更新计算对象,还是更新原始对象?

33qvvth1  于 2023-01-05  发布在  Vue.js
关注(0)|答案(2)|浏览(151)

我使用computed()方法向ref()对象数组添加一些数据。
使用这个对象的计算数组可以阅读数据,例如使用v-for,但是当我试图更新数据时,它被忽略了(什么也没发生),下面的例子显示了工作代码和不工作代码。
在useCart可组合对象(见下面的代码)中,我创建了一个computedCartItems,它Map购物车并为每个商品添加totalPrice。现在,在index.vue文件中,我尝试增加cartItem的金额,如果使用<div v-for="cartItem in cart">循环cart,这将起作用,但使用计算对象<div v-for="cartItem in computedCartItems">时,它将被忽略

使用购物车.js

const useCart = () => {
    const cart = ref([])

    const computedCartItems = computed(() => {
        return cart.value.map(cartItem => {
            return {
                ...cartItem,
                totalPrice: cartItem.amount * cartItem.price
            }
        })
    })

    return {
        cart,
        computedCartItems,
    }
}

export default useCart

index.vue(不工作,使用计算的“computedCartItems”对象)

<div v-for="cartItem in computedCartItems">
    <div>
        <div>{{ cartItem.name }}</div>
        <button @click="onIncrement(cartItem)">+</button>
    </div>
</div>

<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
    const shoppingCartItemIndex = computedCartItems.value.findIndex(item => item.id === id)
    computedCartItems.value[shoppingCartItemIndex].amount++
}
</script>

index.vue(工作,使用原始“cart”对象)

<div v-for="cartItem in cart">
    <div>
        <div>{{ cartItem.name }}</div>
        <button @click="onIncrement(cartItem)">+</button>
    </div>
</div>

<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
    const shoppingCartItemIndex = cart.value.findIndex(item => item.id === id)
    cart.value[shoppingCartItemIndex].amount++
}
</script>
lsmepo6l

lsmepo6l1#

您正在更新原始对象副本上的值。它们没有链接,因此原始对象不会收到更新后的值。

详细回答

Computed为只读。它们是派生数据,不应更新。
因为这是javascript,你可以通过引用来更新对象属性,但是你真的不应该这样做,这是一个不好的做法,会导致不清楚的副作用。
请参阅计算的 typescript 类型:

export declare interface ComputedRef<T = any> extends WritableComputedRef<T> {
    readonly value: T;
    [ComputedRefSymbol]: true;
}

所以myComputed.value是只读的,不能赋值给其他值,你仍然可以赋值给myComputed.value.myProperty = 'foo',但是,正如前面提到的,这是一个不好的做法。
有关此方面的更多信息,请参阅正式文件

一个可能的解决方案

为每个项目(而不是整个购物车)创建可组合的totalPrice,并在您的item对象中分配计算的。

const useItem = (reactiveItem) => {
  const totalPrice = computed(() => reactiveItem.amount * reactiveItem.price)
  
  // Assign a new property in your item, which is the derived totalPrice
  reactiveItem.totalPrice = totalPrice

  return reactiveItem
}

const useCart = () => {
  const cart = ref([])

  // Export a custom function to include the item and make it reactive + use composable (saves the final client from doing it)
  const addItem = (item) => {
    cart.value.push(useItem(reactive(item)))
  }

  return { cart, addItem }
}

const { cart, addItem } = useCart()

function createItem() {
  addItem({ amount: 5, price: 10 })
}

用一个工作示例检查这个在线Playground。
我相信还有其他的方法可以做到这一点,这只是其中之一。例如,你可以使用watch来响应购物车的变化。

li9yvcax

li9yvcax2#

核心问题

计算参考是衍生数据:它以某种方式代表您的数据;不是直接更新它,而是更新它的源。
在文档中有一个关于这个问题的章节非常简洁地解释了这个问题:

避免改变计算值

从计算属性返回的值是派生状态。请将其视为临时快照-每次源状态更改时,都会创建一个新快照。更改快照没有意义,因此应将计算返回值视为只读且永远不会更改-而是更新它所依赖的源状态以触发新计算。
在您的非工作示例中,您没有尝试更新实际计算的ref(这甚至是不可能的;参见答案末尾的参考文档);您正在更新ref值的 properties,您 * 可以 * --但不应该--这样做。然而,除了所有其他问题之外,计算值不会更新,因为总价基于cart中的原始项目,而不是计算值中的项目,这意味着永远不会触发更新(因为cart没有更改)。
如果改为修改源ref(cart),则计算的ref将更新,示例将正常工作:

<!-- Use `computedCartItems` here -->
<div v-for="cartItem in computedCartItems">
    <div>
        <div>{{ cartItem.name }}</div>
        <button @click="onIncrement(cartItem)">+</button>
    </div>
</div>

<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
    // Use `cart` here.
    const shoppingCartItemIndex = cart.value.findIndex(item => item.id === id)
    cart.value[shoppingCartItemIndex].amount++
}
</script>

一种(可能)更好的方式

虽然这样做是可行的,但它很可能不是解决特定问题的理想方法,因为每次更新一个项时,整个计算数组和其中的每个项都将重新创建,这是非常低效的。
相反,您可以让useCart组合只返回单个cart ref沿着一些操作cart的方法。

import { ref, reactive, computed, readonly } from 'vue'

const useCart = () => {
  const cart = ref([])

  /**
  Add a new item to the cart.
  
  Makes the item reactive (so that there is a reactive source for computed properties),
  adds the `totalPrice` computed property, and appends it to the cart array.
  */
  const addItem = (item) => {
    const reactiveItem = reactive(item)
    reactiveItem.totalPrice = computed(() => reactiveItem.amount * reactiveItem.price)
    cart.value.push(reactiveItem)
  }
  
  /**
  Increase the amount of an item.

  You could add all kinds of methods like these.
  */
  const increaseAmount = (id) => {
    const index = cart.value.findIndex((item) => item.id === id)
    cart.value[index].amount += 1
  }

  return {
    cart: readonly(cart), // So that the cart cannot be modified directly by the consumer.
    addItem,
    increaseAmount
  }
}

const { cart, addItem, increaseAmount } = useCart()

addItem({ id: "1", amount: 5, price: 10 })

console.log(cart.value[0].totalPrice) // 50

现在cart的处理是由useCart可组合组件完成的,通过抽象内部组件使消费者的工作更容易。除了上面提到的好处,这还意味着可组合组件仍然可以控制其数据,因为cart引用不能被修改。“关注点分离”等。

文档参考等

Vue文件

Computed Properties - Vue.js Docs
计算引用的要点在于它们会根据其来源自动更新。你不需要直接修改它们,你需要修改它们的来源。
computed属性自动跟踪其React依赖项。Vue知道publishedBooksMessage的计算依赖于author.books,因此当author.books更改时,它将更新依赖于publishedBooksMessage的任何绑定。
不能将值赋给常规计算引用。
默认情况下,计算属性是仅getter属性。如果尝试为计算属性分配新值,将收到运行时警告。在极少数情况下,如果需要“可写”计算属性,可以通过同时提供getter和setter来创建计算属性。
我强烈推荐阅读"Reactivity Fundamentals" section of the Vue Guide,特别是参见“ReferUnwrappinginReactiveObjects”,了解如何在reactive中嵌套计算的ref。
我还建议你准备好的时候,浏览一下整个"Reactivity in Depth" page,它会让你了解React系统实际上是如何工作的。

其他链接

VueUse是一个伟大的资源,无论是对许多得心应手的可组合和学习。

相关问题