Vue3 使用 watch 和 watchEffect 监听响应式数据
import { watch, watchEffect } from 'vue'
watch 用法
watch 函数可以监听单个或多个数据源,默认是当数据发生变化时才执行回调函数 它是惰性的,如果想在数据初始化时就执行 需设置 { immediate: true }
watch 官方指南
监听单个响应式数据
1. 监听 ref 类型
const times = ref(0)
// 数组
const mediaList = ref([
{ id: 1, url: '/data/6876/fgha.png' },
{ id: 2, url: '/data/2513/abcd.png' },
])
watch(times, (newVal, oldVal) => {
console.log(times)
})
watch(mediaList, (newVal, oldVal) => {})
2. 监听 reactive 类型
对于 reactive 类型,vue3 会自动将 deep 设置为 true,所以在外部额外设置是不会起作用的
const obj = reactive({
name: 'Jean',
age: 18
})
watch(obj, (newVal, oldVal) => {
console.log('监听obj', newVal.age)
})
// 不需要设置deep,设置了也无效!
// watch(obj, (newVal) => {}, { deep: true })
注意,watch 不能直接监听 reactive 对象的属性,比如 watch(obj.name, (newVal, oldVal) => {}) 是错误的!相当于传入一个非响应的数值
有两种方式解决:1只需传入一个函数并返回需要监听的属性,2用 toRefs 去解构再监听
const pageData = reactive({ current: 1, rows: 15 })
// 1-常用的
watch(() => pageData.current, (newVal, oldVal) => {})
// 2-或用 toRefs 解构为响应的数据
const { current } = toRefs(pageData)
watch(current, (newVal, oldVal) => {})
4. 监听 computed 计算属性
const memberInfo = computed(() => `用户名:${obj.name},年龄:${obj.age}`)
watch(memberInfo, (newVal) => {
console.log(newVal)
})
5. 监听组件 props
const props = defineProps({
epuId: { type: String, required: true }
})
watch(() => props.epuId, (newVal) => {})
6. 监听 pinia 中的 state
const store = useGlobalStore()
watch(() => store.userInfo, (newVal) => {
if (newVal) {
// do something...
}
}, { immediate: true })
监听多个响应式数据
对于多个数据源,需要传入数组形式,注意 回调函数的参数 newValue 此时为数组,可以将数据的新值解构出来
watch([num, loading], ([newNum, newLoading], [oldNum, oldLoading]) => {
console.log(newNum, newLoading)
})
监听对象的多个特定属性,或不同对象的几个属性
watch([
() => obj1.a,
() => obj2.b
], (newValArr) => {
console.log(...newValArr)
})
数据初始化时 立刻执行
设置 immediate 为 true 该数据初始化时就会立即执行。比如我们需要在项目里监听路由变化,在组件初始化时就拿到 route.path
import { useRoute } from 'vue-router'
const route = useRoute()
watch(route, (newVal) => {
console.log(newVal.path)
}, {
immediate: true
})
watchEffect 用法
自动收集依赖
不需要指定需要监听的数据,会自动监听回调函数中的响应式数据,初始化时会立刻执行
watchEffect(() => {
console.log('watchEffect', obj, num)
})
停止监听
const stop = watchEffect(() => {
console.log('watchEffect', obj, num)
})
const handleSuspend = () => {
stop() // 停止监听
}
清除函数
比如在组件卸载 unmounted 时解绑事件
watchEffect((onInvalidate) => {
window.addEventListener('click', handleClick)
onInvalidate(() => {
window.removeEventListener('click', handleClick)
})
})
是不是和 React 的 useEffect 用法很相似呢
import { useEffect } from 'react'
useEffect(() => {
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
源码解析
想要更深入理解和使用 watch 和 watchEffect,我们需要去源码里大概了解实现原理
watch 源码
先来看看 watch 的源码部分,以下代码非完整源码
// apiWatch.ts
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
// 在DEV环境,cb不是函数类型就输出提示(省略)
if (__DEV__ && !isFunction(cb)) {}
return doWatch(source as any, cb, options)
}
watch 函数接收三个参数:source 监听的数据源,cb 回调函数,options 监听选项设置,函数会执行并返回 doWatch 函数的返回值
和 watchEffect 相比有以下特点:
- 惰性执行副作用
- 更具体说明了应触发侦听器重新运行的状态
- 访问被侦听状态的旧值和当前值(旧值由闭包缓存)
source
下面是源码中 source 的类型
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T) // 单个数据源
type MultiWatchSources = (WatchSource<unknown> | object)[] // 多个数据源
可以看出数据源类型支持单个 Ref 实例、响应式对象 ComputedRef 实例 和 返回相同泛型类型的 effect 函数,还支持以数组形式传入多个数据源
cb
上面 cb 类型虽然是 any,但它有自己的特定类型
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onInvalidate: InvalidateCbRegistrator
) => any
value 和 oldValue 为新旧值,onInvalidate 为一个清除副作用的函数
let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
options
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
}
WatchOptions 继承了 WatchOptionsBase,所以不仅可以传入 immediate、deep 还可以传入 WatchOptionsBase 中的参数控制副作用执行的行为(flush、onTrack、onTrigger)
以下是类型声明
export declare interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync';
}
export declare interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void;
onTrigger?: (event: DebuggerEvent) => void;
}
- immediate 是否立刻执行(默认值为
false) - deep 是否进行深度监听
- flush 控制副作用的处理时机,有三个值:
sync(同步的)、pre(这是默认值,在组件更新前)、post(在组件更新之后) - onTrack 和 onTrigger 用于在开发环境下调试监听器行为的相关入口
watchEffect 源码
watchEffect 是 watch 的衍生,它可以响应式追踪依赖数据,在数据变更时重新执行回调函数
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
doWatch
watch 和 watchEffect 最终都调用了 doWatch 函数,先看下他的函签名
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
instance = currentInstance
): WatchStopHandle
相比 watch 多了一个 instance 参数,它的默认值为 currentInstance,可以理解为当前执行这个方法的组件
doWatch 内部逻辑代码贴出来,里面加了比较详细的注释,有兴趣和时间可以看看:
// 这个函数用于提示非法数据源
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`
)
}
// 定义一个getter函数用来获取要监听的数据
let getter: () => any
// 是否需要强制触发
let forceTrigger = false
// 当数据源source是ref时
if (isRef(source)) {
getter = () => (source as Ref).value // 去取对应数据.value
forceTrigger = !!(source as Ref)._shallow
}
// 数据源是reactive,去取对应数据
else if (isReactive(source)) {
getter = () => source
deep = true // 将deep设为true
}
// 为数组形式的多数据源时
else if (isArray(source)) {
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
// traverse会递归去获得reactive对象的所有键值,
// 因为只要访问一个键就会把这个键依赖收集住,递归把所有键全部都触发依赖收集
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
}
// 为函数时
else if (isFunction(source)) {
// 传入getter、effect函数
if (cb) {
// 到这里说明是watch
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// 到这里说明是watchEffect
getter = () => {
// instance是全局变量 为执行这个方法的组件实例,当组件卸载unmounted就终止
if (instance && instance.isUnmounted) {
return
}
// cleanup是清理函数(它在下面代码中声明)
if (cleanup) {
cleanup()
}
return callWithErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
}
} else {
// 数据源异常提示
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
// deep在reactive数据中会被修改为true
// 因此对于多数据源,reactive数据默认是深度观测的
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
// onInvalidate传入的回调函数fn,被注册到当前effect的onStop里,又传入到了cleanup
// 所以调用cleanup就可以调用fn了
let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (__NODE_JS__ && isInSSRComponentSetup) {
// 服务端渲染的逻辑
}
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
// 调度方法,在数据源改变后 因为响应式系统的trigger调用,内部会将数据源的新旧值传入cb中调用
const job: SchedulerJob = () => {
if (!runner.active) {
return
}
if (cb) {
// watch(source, cb)
// 调用runner => getter => 数据源的最新值
const newValue = runner()
if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
// 将newValue和oldValue作为参数传入cb调用
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate
])
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
let scheduler: ReactiveEffectOptions['scheduler']
// 根据flush模式优化调度方式
if (flush === 'sync') {
scheduler = job
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job()
}
}
}
// 声明effect
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler // 依赖被触发时的调度方法
})
// 将effect推入到当前组件effect队列中
recordInstanceBoundEffect(runner, instance)
// 初始时运行
// 是watch时
if (cb) {
// 因为watch默认为惰性,immediate为true时就立刻调用job
if (immediate) {
job()
}
// 进行effect依赖收集
else {
oldValue = runner()
}
}
// 将effect推入到post队列中,后面的微任务执行会检查post队列
// 如果有任务就执行effect去追踪依赖
else if (flush === 'post') {
queuePostRenderEffect(runner, instance && instance.suspense)
}
// 是watchEffect时
else {
runner()
}
// watchEffect会返回一个函数
return () => {
// 将effect的active属性设false,再次调用时当active=false时就不会执行回调函数
stop(runner)
if (instance) {
// 从组件effect队列里删除这个effect
remove(instance.effects!, runner)
}
}