Vue3 - 使用 computed() 计算属性响应式更新数据

三葉Leaves Author
情境

为了实现页面上的数据和 JS 内部的变量值同步更新、双向绑定,我们当然可以使用 ref()reactive() 配合 v-model 达成这一点。
但是问题在于,如果一个值(设为 x )依赖于其他变量共同构成,这时候我们期望的行为是:

  • 其他变量变化,x 也应当对应变化
  • 甚至更高级的,x 变化,反过来影响其他值也变

但是实际上:

  • 由于 x 在第一次定义的时候就已经计算完成,即使其他值变了,并没有什么东西去提醒 x :你该重新算算你自己是谁了!
  • 反之, x 变了,也没什么东西去通知其他值改变(虽然这种情况可能比较少)

最后的结果就是,无论是页面上还是 JS 变量的值,都不会因为彼此的变化触发对方的更新。为了解决这个问题, computed() 方法应运而生。

情境分析

下面的代码模拟了上面情境所提到的困境。

提供了三个输入框,分别用于测试在页面上修改姓、名和全名。

期望的状态是姓和名的修改应该影响全名的变化,反之亦然。

1
2
3
4
5
6
7
8
9
<template>  
<div class="firstName">Your First Name here: {{ firstName }}</div>
<input type="text" v-model="firstName">
<div class="firstName">Your Last Name here: {{ lastName }}</div>
<input type="text" v-model="lastName">
<div class="firstName">Now I get Your Full Name: {{ fullName }}</div>
<input type="text" v-model="fullName">
<button @click="changeFullName">Click me to change full name</button>
</template>

JS 部分,我们暂时用 ref() 包裹全名,并且提供了一个按钮,点击可以改一下全名的值(得益于 v-model ,用页面上输入框也能改)

1
2
3
4
5
6
7
8
9
10
11
12
13
<script lang="ts" setup>  
import {computed, ref} from 'vue'

const firstName = ref('leaves')
const lastName = ref('webber')

// 全名是由 姓 + 名 以及一个连字符组成
const fullName = ref(firstName.value + '-' + lastName.value)

const changeFullName = () => {
fullName.value = `JO-KE`
}
</script>

测试的结果是,姓和名确实能改,全名也能改,而且也能响应式的渲染到页面上,唯独没达到预期的是,并不能通过更改姓和名影响全名的值
至于为啥,文章开头的情境部分已经说过了。

用法

computed() 有两种用法。

1. 传入单个函数,返回值只读

还是以刚才改全名的例子,我们这次把代码改写成这样,不用 ref() 而是改用 computed() 包裹:

1
2
// 改写例子中的全名赋值语句
const fullName = computed(() => firstName.value + '-' + lastName.value)

可以看到我们往 computed() 括号里塞了一个箭头函数。

在传入单个函数的情况下,传入的函数只需要做一件事:

  • 交代清楚被赋值的变量的值是怎么构成并且计算出的。(这有点像说清楚一道菜是由哪些原材料,经过哪些步骤做出来的)

比如这个例子中,传入的函数就写清楚了全名是怎么算出来的。

关于返回值和“只读”

返回值

那这时候的 fullName 到底是啥?

值得注意的是,虽然其看起来像个字符串,但是我们 log 一下:

1
console.log(fullName)

会发现其实返回的东西是一个 ComputedRefImpl ,长这样:

这个 ComputedRefImpl 有点像 ref() 返回的 RefImpl (Reference Implementation) 类型,但是有一个关键区别:

其本身不存值(浏览器控制台的 value 那边是灰灰的,可能也在暗示这一点),你用 fullName.value 访问的那个值是他根据“菜的原材料和制作过程”实时计算出来的。

不过尽管如此,你依然可以通过 .value 访问其值,就像对其他 ref() 定义的数据一样。

下面这张表简单归纳了一下三者关系:

名称 reactive() 返回值 ref() 返回值 computed() 返回值
内部实现类 (简化) JavaScript Proxy RefImpl ComputedRefImpl
角色 数据源 (对象) 数据源 (通用) 派生数据 (计算)
处理数据类型 对象、数组 任何类型 任何类型(计算结果)
访问方式 obj.prop refVar.value computedVar.value

由于返回的是类似于 RefImpl 的东西,所以 computed 计算出来的值也是响应式的,计算好以后会触发页面的渲染。

只读

只读意味着你不能对其像这样赋值:

1
2
// ✖错误
fullName.value = `JO-KE`

浏览器会警告:
[Vue warn] Write operation failed: computed value is readonly.

如果你想直接 fullName = 'xxx' 那就更不可能了。
仔细看看其声明语句,关键字是 const ,因为其是引用数据类型,更改的是其属性的值。

其实仔细想想就会发现问题了。就拿全名这个案例举例,你更改姓和名,由于你之前定义过计算全名的规则,程序自然能推断出全名的新值。
但是你把全名改了,却没说清楚对应的怎么反过来影响姓和名,那程序怎么能知道?

正因如此,computed() 的第二个用法就来了:

2. 传入包含 get() 函数和 set() 函数的对象,返回值可读写

我们进一步改写之前的赋值语句,这次传入一个对象,里面包含了两个函数。

其中 get() 函数其实等同于只传一个函数时的那个函数,而 set() 则说清了当 .value 被重新赋值的时候,程序应该怎么做。

1
2
3
4
5
6
7
8
9
10
11
let fullName = computed({  
get() {
// 怎么通过原材料做菜?
return firstName.value + '-' + lastName.value
},
set(val) {
// 怎么把拿到的新菜解析成新的原材料?
firstName.value = val.split('-')[0]
lastName.value = val.split('-')[1]
}
})

当“原材料”(在这个例子中是姓和名)发生变化的时候,computed() 会自动监测到,并且重新计算出 fullName 的新值。

fullName 被赋值的时候,被赋的值会作为形参传递给 set() 。而 set() 中又正好定义了怎么处理由“菜”到“原材料” 的逆向过程(尽管这过程不常用),如此一来“原材料”的值也可以因此发生变化了,到这里,形成了完美的闭环。

特性

缓存性质

即使你多次调用计算出来的值,computed() 也不会反复执行其中的语句。

仅仅当你对“原材料”进行更改以后,其才会触发重新计算和渲染。

个人感受

不得不感叹尤雨溪研发这些工具和函数时候的脑洞,能设计出这么精妙绝伦且实用的系统,真是不简单。

  • 标题: Vue3 - 使用 computed() 计算属性响应式更新数据
  • 作者: 三葉Leaves
  • 创建于 : 2024-09-28 00:00:00
  • 更新于 : 2025-06-21 12:01:45
  • 链接: https://blog.oksanye.com/38412b1cee96/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论