计算属性
在之前的文章中,实现过computed,如下
function computed(getter) {
let value // 用来缓存上一次计算的值
let dirty = true // dirty标志 用来表示是否重新计算值
const effectFn = effect(getter,
{
lazy: true,
scheduler() { //调度器 这样子当响应式数据发生变化时会执行
if (!dirty) {
dirty = true
trigger(obj, 'value') // 当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应
}
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
// 当读取value时,手动调用track函数进行追踪(为了解决:effect嵌套导致的响应式数据变化但是副作用不执行)
track(obj, 'value')
return value
}
}
return obj
}
通过上面的代码,computed的优势在于计算属性缓存,当计算属性的依赖项发生变化时,计算属性才会重新计算,有助于避免不必要的重复计算
为什么需要缓存?
// 计算属性缓存
< script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</ script >
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
// 通过调用这样的一个函数也会获得和计算属性相同的结果:
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}
<p>{{ calculateBooksMessage() }}</p>
以上两种方法在结果上确实是完全相同的,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算,这意味着只要author.books不改变,无论多少次访问publishedBooksMessage都会立即返回先前的计算结果,而不重复执行getter函数!
而方法调用总是会在重新渲染发生时再次执行函数
缓存的好处是什么呢?
假如我们有一个非常耗性能的计算属性list,需要循环遍历一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于list,没有缓存的话,会重复执行非常多次list的getter,然而这实际上没有必要
-
Getter不应该有副作用
计算属性的getter应只做计算而没有任何其他的副作用。这一点很重要,不要在getter中做异步请求或者更改DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此getter的职责应该仅为计算和返回该值,应使用
侦听器根据其他响应式状态的变更来创建副作用 -
避免直接修改计算属性值
从计算属性返回的值是派生状态,可以把它看作是一个‘临时快照’,每当源状态发生变化时,就会创建一个新的快照,更改快照是没有意义的,因此计算属性的返回值应该被视为是只读的,并且永远不应该被更改
-
运用场景
假设有一个需求是在输入框中输入一段文字,并在另一个地方显示这段文字的长度。可以使用
computed属性来计算这段文字的长度;假设有一个购物车,computed属性可以方便地计算购物车中商品总价...
侦听器
watch()
在之前的文章中实现过watch
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
let cleanup // cleanup用来存储用户注册的过期回调
function onInvalidate(fn) {
cleanup = fn // 将过期回调存储到 cleanup中
}
const job = () => {
newValue = effectFn()
if (cleanup) {
cleanup()
}
cb(newValue, oldValue, onInvalidate) // 将onInvalidate作为回调函数的第三个参数 以便用户使用
oldValue = newValue
}
const effectFn = effect(
() => getter(),
{
lazy: true,
scheduler: () => {
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
// 例子
let finalData
watch(obj, async (newVal, oldVal, onInvalidate) => {
let expired = false
onInvalidate(() => {
expired = true
})
const res = await fetch('')
if (!expired) {
finalData = res
}
})
obj.foo++
setTimeout(() => {
obj, foo++ // 200ms后做第二次修改
}, 200)
当我们需要在状态发生变化时执行一些‘副作用’,例如更改DOM或是根据异步操作的结果去修改另一处的状态,在组合式API中,可以只用watch函数在每次响应式状态发生变化时触发回调函数
watch的第一个参数,可以是一个ref(包括计算属性)、一个响应式对象、一个getter函数、多个 数据源 组成的数组,但是注意不能直接侦听响应式对象的属性值:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
// !错误 因为watch()得到的参数是一个number
const obj = reactive({ count: 0 })
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
// 这里需要一个返回该属性的getter函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
-
深层侦听器
直接给watch传入一个响应式对象,会隐式地创建一个深层侦听器--该回调函数在所有嵌套的变更时都会被触发
const obj = reactive({ count: 0 }) watch(obj, (newValue, oldValue) => { // 在嵌套的属性变更时触发 // 注意:`newValue` 此处和 `oldValue` 是相等的 // 因为它们是同一个对象! }) obj.count++ -
即时回调的侦听器
watch默认是懒执行的,回顾我们实现watch,会在配置项中添加
lazy: true,即仅当数据源变化时,才会执行回调。如果需要回调立即执行,可以通过传入immediate: truewatch( source, (newValue, oldValue) => { // 立即执行,且当 `source` 改变时再次执行 }, { immediate: true } ) -
运用场景
(权限管理)监听用户权限数据的变化,根据用户的权限动态展示或隐藏页面内容或功能按钮;监听状态变化触发定时器的开启、关闭或者异步任务的启动、暂停...
watchEffect()
-
watchEffetc函数允许我们自动追踪回调的响应式依赖,即不需要手动添加依赖项
// watch() const todoId = ref(1) const data = ref(null) watch( todoId, async () => { const response = await fetch( `https://jsonplaceholder.typicode.com/todos/${todoId.value}` ) data.value = await response.json() }, { immediate: true } ) // watchEffect() watchEffect(async () => { const response = await fetch( `https://jsonplaceholder.typicode.com/todos/${todoId.value}` ) data.value = await response.json() })该回调会立即执行,不需要指定
immediate: true在执行期间,它会自动追踪
todoId.value作为依赖(和计算属性类似),每当todoId.value发生变化时,回调会再次执行有了watchEffect(),我们不再需要明确传递todoId作为源值
-
当只有一个依赖项的例子来说,watchEffect()的好处相对较小,但是对于多个依赖项的侦听器来说,使用watchEffect()可以消除手动维护依赖列表的负担。此外,如果需要侦听一个嵌套数据结构中的几个属性,watchEffect()可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性
-
注意:watchEffect仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个await正常工作前访问到的属性才会被追踪
| watch | watchEffect | ||
|---|---|---|---|
| 相同点 | 都能响应式地执行副作用的回调 | ||
| 不同点:追踪响应式依赖的方式 | 只追踪明确侦听的数据源 不会追踪任何在回调中访问到的东西 仅在数据源确实改变时才会触发回调 | 会在副作用发生期间追踪依赖 它会在同步执行过程中,自动追踪所有能访问到的响应式属性 | |
| 优缺点 | 会避免在发生副作用时追踪依赖,因此可以更加精确地控制回调函数的触发时机 | 方便,代码往往更加简洁,但是有时其响应式依赖关系会不那么明确 |
回调的触发时机
-
当更改了响应式状态,它可能会同时触发Vue组件更新和侦听器回调
-
默认情况下,用户创建的侦听器回调,都会在Vue组件更新之前被调用,若项在侦听器回调中能访问被Vue更新之后的DOM,需要指明
flush:'post'选项watch(source, callback, { flush: 'post' }) watchEffect(callback, { flush: 'post' }) // 后置刷新的watchEffect有个方便的别名 import { watchPostEffect } from 'vue' watchPostEffect(() => { /* 在 Vue 更新后执行 */ })
参考文章: