红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

Vue3 学习笔记 3:计算属性

2025年8月31日 8点热度 0人点赞 0条评论

单向绑定和双向绑定

看一个示例:

<template>
    <div class="person">
        姓:<input type="text" v-bind:value="firstName" />
        <br />
        名:<input type="text" v-bind:value="lastName" />
        <br />
        全名:????
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { ref } from 'vue'
const firstName = ref('zhang')
const lastName = ref('san')
</script>

在这个示例中,需要让用户分别输入姓和名,然后展示完整的姓名。

可以通过数据绑定v-bind:value将setup中定义的响应式数据绑定到value属性,也可以简写为:

<input type="text" :value="firstName"

但这样做是单向绑定,数据改变可以影响视图,但视图中的用户输入不会改变数据,因此需要使用双向绑定:

<input type="text" v-model="firstName" />

computed

在这个示例中可以通过差值表达式显示全名:

全名:{{ firstName }} - {{ lastName }}

但这样做有一个缺点,如果需要对全名进行额外处理,比如姓和名的首字母大写:

<template>
    <div class="person">
        姓:<input type="text" v-model="firstName" />
        <br />
        名:<input type="text" v-model="lastName" />
        <br />
        全名:{{ firstCharUp(firstName) }} - {{ firstCharUp(lastName) }}
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { ref } from 'vue'
const firstName = ref('zhang')
const lastName = ref('san')
const firstCharUp = (word:string) => {
    console.log('firstCharUp')
    return word.charAt(0).toUpperCase() + word.slice(1)
}
</script>

差值表达式的内容就比较复杂,违背了模版尽量简洁的设计原则。此外还有个更致命的结果,因为上面的示例中,差值表达式直接使用了自定义方法firstCharUp,如果我们需要多次展示全名:

        全名:{{ firstCharUp(firstName) }} - {{ firstCharUp(lastName) }}
        全名:{{ firstCharUp(firstName) }} - {{ firstCharUp(lastName) }}
        全名:{{ firstCharUp(firstName) }} - {{ firstCharUp(lastName) }}

在这里自定义方法就会调用多次(6),且你不能简单的通过将方法保存为变量并展示变量的方式替代,因为在这里firstName和lastName是两个响应式数据,需要随着用户输入的改变而改变。

因此这里需要使用计算属性(computed):

<template>
    <div class="person">
        姓:<input type="text" v-model="firstName" />
        <br />
        名:<input type="text" v-model="lastName" />
        <br />
        全名:{{ fullName }}
        全名:{{ fullName }}
        全名:{{ fullName }}
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { ref,computed } from 'vue'
const firstName = ref('zhang')
const lastName = ref('san')
const firstCharUp = (word:string) => {
    console.log('firstCharUp')
    return word.charAt(0).toUpperCase() + word.slice(1)
}
const fullName = computed(() => {
    return firstCharUp(firstName.value) + ' - ' + firstCharUp(lastName.value)
})
</script>

computed方法接受一个匿名函数,该匿名函数返回一个值,这个值就是利用其它响应式数据计算得出的。每当原始的响应式数据的值变化时,computed中的匿名函数就会被调用,以计算新的计算属性的值。换言之,computed有缓存机制,如果涉及的原始响应式数据没有改变,用于计算的匿名函数就不会被调用。

这种方式定义的计算属性是只读的:

<template>
    <div class="person">
        // ...
        <button @click="changeFullName">修改全名</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
// ...
const changeFullName = () => {
    fullName.value = 'li-si'
}
</script>

触发按钮点击事件会报错:

Person3.vue:25  [Vue warn] Write operation failed: computed value is readonly

如果要定义可读可写的计算属性,需要给computed方法传入一个同时具有get和set方法的对象:

const fullName = computed({
    get() {
        return firstCharUp(firstName.value) + ' ' + firstCharUp(lastName.value)
    },
    set(newValue:string) {
        const names = newValue.split('-')
        firstName.value = names[0]
        lastName.value = names[1]
    }
})

需要获取计算属性的值时,get方法就会被触发(有缓存),需要修改计算属性的值时(比如fullName.value = 'li-si'),set方法就会被触发。

watch

watch可以监视以下四种数据:

  • ref定义的数据

  • reactive定义的数据

  • 返回一个值的函数(getter 函数)

  • 一个包含以上内容的数组

监视ref定义的基本类型数据

看一个示例:

<template>
    <div class="person">
        {{ total }}
        <br></br>
        <button @click="totalIncrease">点我+1</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { ref } from 'vue'
const total = ref(0)
const totalIncrease = () => {
    total.value += 1
}
</script>

每次触发点击事件total都会自增,如果要在total自增时进行监视:

<script setup lang="ts" name="Person">
import { ref,watch } from 'vue'
const total = ref(0)
const totalIncrease = () => {
    total.value += 1
}
watch(total, (newValue, oldValue) => {
    console.log(newValue, oldValue)
})
</script>

watch方法接收两个参数,第一个参数是被监视的ref定义的响应式数据,第二个参数是监视被触发时的回调函数。这个回调函数有两个参数,分别是新的值和旧的值。

上面示例中的监视是一直有效的,实际上watch方法是有返回值的,其返回值是一个函数,通过这个函数可以在需要的时候关闭监视:

const stopWatch = watch(total, (newValue, oldValue) => {
    console.log(newValue, oldValue)
    if (newValue > 10) {
        stopWatch()
    }
})

监视ref定义的对象类型数据

看一个示例:

<template>
    <div class="person">
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <button @click="changeName">修改姓名</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { ref,watch } from 'vue'
const person = ref({
    name: '张三',
    age: 18
})
const changeName = () => {
    person.value.name += '~'
}
const changeAge = () => {
    person.value.age += 1
}
const changePerson = () => {
    person.value = {
        name: '王五',
        age: 20
    }
}
</script>

添加一个监听:

watch(person, (newValue, oldValue) => {
    console.log('oldValue', oldValue)
    console.log('newValue', newValue)
})

实际测试会发现,这个监听只会在ref定义的响应式数据的value值被替换为新对象(changePerson方法)时会触发,只修改某个属性(比如changeAge方法)时不会被触发。

如果要监视ref定义的对象类型的响应式数据的具体属性,需要开启深层监视:

watch(person, (newValue, oldValue) => {
    console.log('oldValue', oldValue)
    console.log('newValue', newValue)
},{deep: true})

具体方式是为watch方法添加第三个参数,这个参数是一个对象,可以添加多个属性,其中deep:true表示开启深层监视。

需要额外注意的是,当响应式数据包裹的对象被(新对象)整体替换时,此时的newValue和oldValue都是有意义的,比如:

oldValue Proxy(Object) {name: '张三', age: 18}
newValue Proxy(Object) {name: '王五', age: 20}

但如果只是修改了对象的某个属性:

oldValue Proxy(Object) {name: '王五', age: 21}
newValue Proxy(Object) {name: '王五', age: 21}

此时它们实际上是同一个对象,所以属性值完全相同。

最后,除了deep:true,还有一个常见的选项immediate:

watch(person, (newValue, oldValue) => {
    console.log('oldValue', oldValue)
    console.log('newValue', newValue)
}, { deep: true, immediate: true })

此时在监听被定义后会立即执行一次回调(不需要等到被监听数据发生变化)。

监视reactive定义的数据

看示例:

<template>
    <div class="person">
        <h2>姓名:{{ person.name }}</h2>
        <h2>年龄:{{ person.age }}</h2>
        <button @click="changeName">修改姓名</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="changePerson">修改整个人</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { reactive, watch } from 'vue'
const person = reactive({
    name: '张三',
    age: 18
})
const changeName = () => {
    person.name += '~'
}
const changeAge = () => {
    person.age += 1
}
const changePerson = () => {
    Object.assign(person, {
        name: '王五',
        age: 20
    })
}
</script>

创建监视:

watch(person, (newValue, oldValue) => { 
    console.log('oldValue', oldValue)
    console.log('newValue', newValue)
})

需要注意的是,监视reactive定义的对象类型的响应式数据时,默认开启深层监视(deep:true),因为reactive定义的响应式数据压根没有.value属性,所以浅层监视没有意义。换言之,下面的写法无效且没有意义(虽然不会报错):

watch(person, (newValue, oldValue) => { 
    console.log('oldValue', oldValue)
    console.log('newValue', newValue)
},{deep: false})

此时深层监视依然有效。

与ref定义的对象类型的响应式数据不同,reactive定义的对象类型的响应式数据没有.value属性,关闭了深层监视就没有东西可以监视了。

监视对象类型的响应式数据的属性

示例:

<template>
    <div class="person">
        <h2>姓名:{{ person.name }}</h2>
        <h2>年龄:{{ person.age }}</h2>
        <h2>车1:{{ person.cars.c1 }}</h2>
        <h2>车2:{{ person.cars.c2 }}</h2>
        <button @click="changeName">修改姓名</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="changeC1">修改第一辆车</button>
        <button @click="changeC2">修改第二辆车</button>
        <button @click="changeCars">修改所有车辆</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { reactive, watch } from 'vue'
const person = reactive({
    name: '张三',
    age: 18,
    cars: {
        c1: '保时捷',
        c2: '法拉利'
    }
})
const changeName = () => {
    person.name += '~'
}
const changeAge = () => {
    person.age += 1
}
const changeC1 = () => {
    person.cars.c1 += '~'
}
const changeC2 = () => {
    person.cars.c2 += '~'
}
const changeCars = () => {
    person.cars = {
        c1: '保时捷~',
        c2: '法拉利~'
    }
}
</script>

如果需要监视的不是整个响应式数据,而是其中的某个基本类型的属性,你可能会这么写:

watch(person.age, (newAge, oldAge) => {
})

但是会报错,必须改写成 getter 函数的形式:

watch(()=>person.age, (newAge, oldAge) => {
    console.log(`年龄由${oldAge}变为${newAge}`)
})

此时只有年龄改变才会触发监视。

如果监视的是对象类型的属性:

watch(person.cars, (newCars, oldCars) => {
    console.log('newCars',newCars)
    console.log('oldCars',oldCars)
})

这么写是没有问题的,但是,此时只会监视person.cars属性的改变,如果person.cars整体被替换掉,就不会触发监视。如果改写成 getter 函数的形式:

watch(() => person.cars, (newCars, oldCars) => {
    console.log('newCars', newCars)
    console.log('oldCars', oldCars)
})

person.cars整体替换会触发监视,但其中属性改变又不会触发。因此,最佳实践是结合使用 getter 函数和深层监视来监视对象类型的属性:

watch(() => person.cars, (newCars, oldCars) => {
    console.log('newCars', newCars)
    console.log('oldCars', oldCars)
},{deep: true})

监视多个数据

如果要同时监视多个数据,可以将这些数据放在一个数组中进行监视:

watch([() => person.age, () => person.cars], (newValue, oldValue) => {
    console.log(newValue, oldValue)
}, {deep: true})

watchEffect

看一个示例:

<template>
    <div class="person">
        <h2>水位:{{ height }}</h2>
        <h2>水温:{{ temp }}</h2>
        <button @click="changeHeight">提高水位</button>
        <button @click="changeTemp">提高水温</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { ref } from 'vue'
const temp = ref(0)
const height = ref(0)
const changeHeight = () => {
    height.value += 10
}
const changeTemp = () => {
    temp.value += 10
}
</script>

添加一个监视,如果水温或水位高于某个阈值,就给服务器发送信息:

watch([height, temp], (newValue, oldValue) => {
    const [newHeight, newTemp] = newValue
    if (newHeight > 100 || newTemp > 50) {
        console.log('水温或水位已经达到阈值')
    }
})

这么做是没有问题的,但如果作为判断条件的需要监视的数据过多,代码就会显得很繁琐。此时可以使用watchEffect简化代码:

watchEffect(() => {
    if (height.value > 100 || temp.value > 50) {
        console.log('水温或水位已经达到阈值')
    }
})

watchEffect不需要指明需要监视的数据,它会自动分析回调函数内的代码,在需要监视的数据发生变化时触发回调。此外,watchEffect定义时会立即执行一次,相当于隐式添加了immediate:true设置。

The End.

参考资料

  • 尚硅谷Vue3入门到实战

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: vue watch
最后更新:2025年8月31日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号