目录
前言
关于 vue2 的 computed 和 watch 请戳这里
在 vue3 的 setup 里使用 computed、watch 和 watchEffect,一定记得先引入 computed、watch 和 watchEffect。
一、setup 里的计算属性(computed)
在 vue3 中,computed 可以在 setup 函数里实现。除了写法不一样,功能上与 vue2 中的 computed 是一致的。
- 只有 getter 时,传入一个回调函数。
- 有 getter 和 setter时,传入一个对象,有 get 和 set 两个属性方法。
- 需要将处理后的值返回作为该计算属性的值。
1、在 setup 里使用 computed 的三种方式
- 直接在 setup 里使用 computed 函数;
- 通过 defineComponent 函数在 setup 里使用 computed 函数;
- 在 <script setup> 里使用 computed 函数。
其中,defineComponent 以及 <script setup> 都是 setup 的语法糖。
(1)、直接在 setup 里使用 computed 函数
例如:
<template>
<div>{{ fullName }}</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('hello')
const lastName = ref('world')
const fullName = computed(() => {
return firstName.value + '-·-' + lastName.value
})
return {
firstName,
lastName,
fullName
}
}
}
</script>
(2)、通过 defineComponent 函数在 setup 里使用 computed 函数
defineComponent 函数是 vue3 的语法糖:
- defineComponent 函数支持 TS 的 “参数类型推断”(如果你使用的是 vue3 + TS,那么使用 defineComponent 将会更友好。)
例如:
<template>
<div>{{ fullName }}</div>
</template>
<script>
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup() {
const firstName = ref('hello')
const lastName = ref('world')
const fullName = computed(() => {
return firstName.value + '-·-' + lastName.value
})
return {
firstName,
lastName,
fullName
}
}
})
</script>
(3)、在 <script setup> 里使用 computed 函数
<script setup> 是 vue3 的新的语法糖,之前的组合 API 相比:
- 之前的组合 API 必须返回
return
,使用 <script setup> 后就不必了。 - 有更好的运行时性能。
例如:
<template>
<div>{{ fullName }}</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('hello')
const lastName = ref('world')
const fullName = computed(() => {
return firstName.value + '-·-' + lastName.value
})
</script>
2、在 setup 里的 computed 的 getter 和 setter
当 computed 有 getter 和 setter 时,需要传入一个对象而不是一个函数作为 computed 的参数,然后在 computed 中实现 get 和 set 两个属性方法。
例如:
<template>
<div> firstName: {{ firstName }} </div>
<div> lastName: {{ lastName }} </div>
<div> fullName: {{ fullName }} </div>
</template>
<script>
import { reactive, toRefs, computed } from 'vue'
export default {
setup() {
const user = reactive({
firstName: 'hello',
lastName: 'world'
})
const fullName = computed({
get() {
return user.firstName + '-·-' + user.lastName
},
set(val) {
const nameList = val.split('-·-')
user.firstName = nameList[0]
user.lastName = nameList[1]
}
})
return {
...toRefs(user),
fullName
}
}
}
</script>
二、setup 里的侦听器(watch、watchEffect、watchPostEffect 和 watchSyncEffect)
推荐优先考虑使用 watchEffect。
- watch:侦听器。
- watchEffect:初始化时会立即执行的侦听器(默认:
immdiate: true
)。 - watchPostEffect:watchEffect() 使用 flush: ‘post’ 选项时的别名。(了解)
- watchSyncEffect:watchEffect() 使用 flush: ‘sync’ 选项时的别名。(了解)
1、watch
在 vue3 中,watch 可以在 setup 函数里实现。除了写法不一样,功能上与 vue2 中的 watch 以及 $watch 是一致的。
setup 里的 watch 完全等同于组件侦听器 property。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
watch 的工作原理:侦听特定的数据源,并在回调函数中执行副作用。它默认是惰性的——只有当被侦听的源发生变化时才执行回调,不过,可以通过配置 immediate 为 true 来指定初始时立即执行第一次。可以通过配置 deep 为 true,来指定深度监视。
watch 的两个属性:
- immdiate: 默认情况下,侦听器需要 data 后面值改变了才会生效,若需要侦听器一进入页面就生效,那就需要使用 immediate。
- deep: 默认情况下,侦听器只会监听数据本身的改变,若要进行深度监听,那就需要使用 deep。
- immediate 和 deep 配置在第三个参数对象里。
例如:
<template>
<div>
<span>姓名:</span>
<input type="text" v-model="name" />
</div>
<div>
<span>年龄:</span>{{ age }}
<input type="button" value="+" @click="age++" />
</div>
</template>
<script>
import { ref, toRefs, watch } from 'vue'
export default {
setup() {
const name = ref('小樱')
const age = ref(0)
const selectLoves = ref([])
const user = ref({ age: 0, gender: '女' })
// 侦听一个 ref
watch(age, (newValue, oldValue) => {
console.log('111', newValue, oldValue)
}, { immdiate: true })
// 侦听多个 ref
watch([name, age], (newValue, oldValue) => {
console.log('222', newValue, oldValue)
}, { immdiate: true })
// 侦听一个数组
watch(selectLoves, (newVal, oldVal) => {
console.log('333', newVal, oldVal)
}, { immdiate: true })
// 侦听对象里的某个属性
watch(
() => user.gender,
(newValue, oldValue) => {
console.log('444', newValue, oldValue)
},
{ immdiate: true, deep: true }
)
// 侦听对象里的多个属性
watch(
[() => user.age, () => user.gender],
(newValue, oldValue) => {
console.log('555', newValue, oldValue)
},
{ immdiate: true, deep: true }
)
return { ...toRefs(user), name, age, selectLoves }
}
}
</script>
直接侦听一个对象时,建议:直接侦听对象里的具体的属性,而不是侦听对象本身。
【注意】:直接侦听一个对象时有 2 点需要注意:
- 无法正确的获取 oldValue,因为:新值变了同时旧值也跟着变了——改变一个对象里属性的值时,新值和旧值指向的是同一块内存区,所以无法拿到旧值,也可以理解为是浅拷贝的问题。
- 默认开启深度监听(
deep: true
),且将 deep 置为 false 无效。
2、watchEffect 函数
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
- watch 默认是惰性的(immedate: false),而 watchEffect 默认会立即执行(immedate: true)并开启深度监听(deep: true)。
- watch 可以访问变化前的值(oldVal)和变化后的值(newVal),而 watchEffect “无法”访问变化前的值(oldVal)。
- watch 只追踪明确侦听的数据源。仅在数据源确实改变时才会触发回调(可以避免在发生副作用时追踪依赖)。watchEffect 只有在回调中使用数据才会进行监听,修改属性是不被监听的(即 get 属性才会监听,set 属性不进行监听)。
例如:
<template>
<h1>Vue3新特性 -watchEffect 监听属性</h1>
<div>
<p>{{name}}</p>
<p>{{nameObj.name}}</p>
</div>
</template>
<script>
import { ref, reactive, watch, watchEffect } from 'vue'
export default {
setup() {
// 监听基本类型
const name = ref('张三')
setTimeout(() => {
name.value = '李四'
}, 1000)
watch(name, (newVal, oldVal) => {
console.log(newVal, oldVal)
}, {immediate: true}) //立即执行
//监听复杂类型
const nameObj = reactive({name: 'zhangsan'})
setTimeout(() => {
nameObj.name = 'list'
}, 2000)
//复杂数据无法直接监听、惰性
watch(() => nameObj, (newVal, oldVal) => {
console.log(newVal, oldVal) //不会触发
})
//需要深度监听、不惰性
watch(() => nameObj, (newVal, oldVal) => {
console.log(newVal, oldVal) //newVal、oldVal具有响应式
}, { deep: true })
//也可以直接监听对象的属性
watch(() => nameObj.name, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
// 同时监听多个对象的属性
watch([() => nameObj.name, () => nameObj.lastName], ([newName, newLastName], [oldName, oldLastName]) => {
console.log(newName, oldName, newLastName, oldLastName)
})
const stop = watchEffect(() => {
console.log(name);
console.log(nameObj.name);
})
// 5秒后停止监听
setTimeout(()=>{
stop()
},5000)
return {
name,
nameObj
}
}
}
</script>
3、vue3 回调的触发时机,以及 watchPostEffect 和 watchSyncEffect
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
vue3 给 watchEffect 提供了 flush 属性,用来调整回调函数的刷新时机。并分别为他们起了别名:
- watchPostEffect:watchEffect() 使用 flush: ‘post’ 选项时的别名。
- watchSyncEffect:watchEffect() 使用 flush: ‘sync’ 选项时的别名。
flush 有 3 个值可以选择:
- pre:默认值,前置刷新——表示在 dom 更新前调用。比如:在 dom 更新前你需要改变某些数据,就使用 pre,这些数据改变完一起更新 dom,提高性能。类似于 vue2 的 beforeCreated 生命周期钩子函数。
- post:后置刷新——表示 dom 更新完成后调用。比如:你要获取 dom 或者子组件,这与在 created 生命周期钩子函数里使用 nextTick() 的效果一样。
- sync:同步调用。在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: ‘sync’ 来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: ‘post’ 选项:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post',
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
上述代码,可简写为:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
同理 watchSyncEffect 也是同理。
【注意】:需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
4、如何停止侦听器?
假设我们定义了下面这个侦听器:
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 输出 0
count.value++
// -> 输出 1
副作用清除:
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel)
data.value = await response
})
停止侦听器:
const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()
三、watch、watchEffect 和 computed 的对比
- watch
- 懒执行副作用——需要手动指明侦听的内容,也要指明侦听的回调。
- 默认 immdiate 是 false,所以初始化时不会执行,仅在侦听的源数据变更时才执行回调。
- 不需要有返回值。
- 可以获得变化前的值(oldVal)。
- watchEffect
- 自动收集依赖,不需要手动传递侦听内容——自动侦听回调函数中使用到的响应式数据
- 默认 immdiate 是 true,所以初始化时会立即执行,同时源数据变更时也会执行回调。
- 不需要有返回值。
- 无法获得变化前的值(oldVal)。
- computed
- 注重的计算出来的值(回调函数的返回值), 所以必须要写返回值。
【参考】
VUE3(十四)使用计算属性computed和监听属性watch
vue3的计算属性与watch
Vue 3 响应式侦听与计算
vue3计算属性(computed)与监听(watch)
vue3的 computed 计算属性 与 watch监听
watchEffect函数