当开发中 我们需要对某个或一些响应式数据改变后,做相关操作,这时候可以使用
watch ,watchEffect api 构建新的 effect 完成 响应式操作。
核心代码 位于 runtime-core/src/apiwatch
// watch 的类型定义
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
///
// overload: watching reactive object w/ cb
export function watch<
T extends object,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// 三个入参
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
1. source 监听源 可以是 Ref ComputedRef 或者一个函数 或者 object
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
2. cb 一个回调函数 会在监听源发生改变后 执行 会有 新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
}
export interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync'
}
export interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
3.options 5个参数 其中 onTrack/onTrigger 只会在开发环境生效 便于开发人员调试。
immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined。
deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。
flush:调整回调函数的刷新时机 后面 会详细分析
export type WatchStopHandle = () => void
本质返回了 一个 可以销毁监听effect 的 函数 后面会给出分析
watch 和 watchEffect 都是调用的这个函数 只是入参不同 watch 多了一个 回调函数
对于 watch 来说 source 是一个 响应式数据 而对 watchEffect 来说 source是一个 函数。
由于 监听源source 会有不同的类型 dowatch 第一步就是去 标准化这些不同类型的source
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
// ...
// source 不合法的时候警告函数
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.`
)
}
const instance = currentInstance
let getter
let forceTrigger = false
let isMultiSource = false
// 判断是不是 ref 类型
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
}
// 判断是不是响应式对象
else if (isReactive(source)) {
getter = () => source
deep = true
}
// 判断是不是数组类型
else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
}
// 判断是不是函数类型
else if (isFunction(source)) {
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// 如果只有一个函数作为source 入参,则执行 watchEffect 的逻辑
// ...
getter = () => {
if (instance && instance.isUnmounted) {
return
}
// 用于清理 effect.stop() 执行完的 副作用 用于清理定时器和取消事件订阅等
// watchEffect 有个 cleanup 的入参函数 就是用来执行 effect.stop 的回调的
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
}
// 都不符合,则告警
else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
// 深度监听
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
// ...
}
结论: 不同类型的source 都会被转化成一个 getter 函数
watch 的 getter 可以看作是一个 访问 响应式数据的 函数
watchEffect 的 getter 可以看作是 一个使用了 响应式数据的 业务执行函数。
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// 有cb时 属于watch,每次执行 cb 是 会先计算拿到 最新的 getter 返回的值,并 进行下一次的依赖收集
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
// 注意 watch cb 第三个参数即是 cleanup 他执行的时机 也是在effect 被销毁的时候
if (cleanup) {
cleanup()
}
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
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect 每次都会重新执行 getter
effect.run()
}
}
// 先根据 job 和传入的 flush 创建 scheduler
// 会在响应式数据发生改变 派发更新的时候 执行 effect.shduler 时触发
let scheduler: EffectScheduler
if (flush === 'sync') {
// 同步执行
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// 创建的任务 会被放在 pendingPostFlushCbs 队列里面 就会比 queue 队列 执行时 机要晚 也就是 dom更新后才执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// job 上带有 pre 会在执行 flushJobs 时 排序到前面 会先于组件更新执行回调
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
const effect = new ReactiveEffect(getter, scheduler)
小结 创建watch 的 options 中的 flush参数的三个值的含义 表示 watch 回调执行的时机不同
sync 表示 在 响应式 派发更新的时候直接就执行了
post 表示在 组件更新微任务完成后执行的
pre 默认 和 组件更新处于同一个微任务队列中,并先于组件更新操作
const unwatch = () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch
// initial run
if (cb) {
// 表示是 watch 逻辑
if (immediate) {
// 立即执行回调 job job 其实是调用 effect.run() 也就是 source 转化的 getter (一个 获取响应式数据的函数),就发生了依赖收集。
job()
} else {
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
// 是watcheffect 逻辑 也是执行 getter
effect.run()
}
总结:
watch 和 watchEffect 最开始 都是根据 getter 发生的首次依赖收集。后续的依赖收集比对 会有所不同 watch 走的是 cb 注册的回调函数 ,而 watchEffect 则还是 getter。
watch 和 watchEffect 本质都是创建了一个新的 ReactiveEffect 来管理响应式等操作,根据 flush 的状态触发 job 的不同阶段更新。