单向绑定和双向绑定
看一个示例:
<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 />
全名:
- </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 />
全名:
全名:
全名:
</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">
<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>姓名:</h2>
<h2>年龄:</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>姓名:</h2>
<h2>年龄:</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>姓名:</h2>
<h2>年龄:</h2>
<h2>车1:</h2>
<h2>车2:</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>水位:</h2>
<h2>水温:</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
设置。
文章评论