vue3 与 vue2 主要差异之一无疑是响应式实现上的改变。本文主要阐述响应式原理的实现方式解析以及核心源码阅读的注释理解。
本文主要记录 watch/watchEffect 的源码阅读记录。api源码路径
packages/runtime-core/src/apiWatch.ts
能阅读到这里,真的很不容易了,我还可以,再接再励!
watchEffect
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
watchEffect 类型定义:
function watchEffect(
effect: WatchEffect, // 副作用函数
options?: WatchOptionsBase // 配置项
): WatchStopHandle // 取消监听函数
type WatchEffect = (onCleanup: OnCleanup) => void
interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync'
}
type WatchStopHandle = () => void
flush:用来设置副作用函数执行时机
pre
:副作用函数在组件渲染前执行,默认post
:副作用函数延迟到组件渲染之后再执行sync
:副作用函数在响应式依赖发生改变时立即执行
返回值<WatchStopHandle>是一个用来停止该副作用的函数。
watchEffect实际执行的是doWatch
函数:
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
doWatch 类型定义:
function doWatch(
// 副作用函数-响应式字段、副作用函数
source: WatchSource | WatchSource[] | WatchEffect | object,
// 回调函数 - watch使用
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle // 取消监听函数
// 监听响应式对象类型
type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
// watch回调函数,接收三参数,新值,旧值,清栈方法
type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
在看 doWatch 完整代码之前,还需要先了解一下调度队列 SchedulerJob/ quequJob,放在另一章节。
全文重点 doWatch
,好好看:
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
// 一些校验
// ...
const instance = currentInstance // 当前组件实例
let getter: () => any // 自带getter方法 - 获取监听字段值的getter函数,用来建立响应字段依赖
let forceTrigger = false
let isMultiSource = false // 监听多个响应字段标识
// 解析第一参数 - 校验第一参数格式 - 设置参数对应的 getter 方法 - 读取监听响应字段值
if (isRef(source)) {
// ref 参数
getter = () => source.value // 自动解包
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// reactive 参数
getter = () => source
deep = true // 递归监听
} else if (isArray(source)) {
// 数组参数
isMultiSource = true // 标识监听多个响应字段
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
// 数组原始可以不同类型,getter需要对各元素进行类型判断,返回字段值
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)) {
// 函数参数,需要执行函数返回监听字段值
// callWithErrorHandling 执行函数并返回,并使用try...catch 拦截函数执行错误信息
if (cb) {
// getter with cb
// 带回调函数cb -> watch
// callWithErrorHandling
getter = () =>
// 执行函数,函数必须返回监听的响应字段,才能建立依赖
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
// 不带回调函数cb,简单的effect副作用函数 -> watchEffect
getter = () => {
// 当前组件已卸载
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
// 异步调用函数 Promise | Promise[]
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
} else {
// 其他类型报错
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
// 2.x array mutation watch compat
// https://v3-migration.vuejs.org/breaking-changes/watch.html
// watch array vue2 差异处理
// vue3 监听数组,如果没有指定deep,只有数组被重新赋值后才会触发回调,不会像vue2一样使用突变函数(push、pop...)也会触发回调。
// 可以通过COMPAT配置(DeprecationTypes.WATCH_ARRAY=true)取消差异
// 但是会得到一个warning
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter()
if (
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
// 取消差异,监听array 未指定deep 也递归监听
traverse(val)
}
return val
}
}
// 递归监听 - reactive默认deep=true
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
// 定义watch清除函数 clearup
let cleanup: () => void
// onClearup 作为cb第三参数,用于注入stop钩子函数
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.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 or sync flush
// 使用ssr不需要创建effect,因此不需要定义 clearup,直接设置未NOOP
// 但是,如果使用同步更新flush:sync,需要定义 clearup
let ssrCleanup: (() => void)[] | undefined
// ...
// 获取监听值
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
// 声明对应的调度任务job
const job: SchedulerJob = () => {
if (!effect.active) {
// 已经清除监听,对应的副作用函数对象 effect.active = false
return
}
if (cb) {
// 需要执行回调
// watch(source, cb)
// 获取新值
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
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 ||
(isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE)
? undefined
: oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect
// 执行副作用函数
effect.run()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
// 带回调支持递归 更新 - watch
job.allowRecurse = !!cb
// 副作用函数对象effect的schedule 调度方法
let scheduler: EffectScheduler
if (flush === 'sync') {
// 同步执行 - 响应字段更新后立即更新
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// 组件更新后才执行
// 加入post调度任务队列pendingPostFlushCbs - 异步更新
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// 组件更新前才执行
// 加入调度任务队列queue - 异步更新
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
// 声明watch对应的副作用函数对象实例effect,传入调度方法。
// 触发 trigger 后执行effect.scheduler而不是effect.run
// watch 把获取监听值方法getter 当成副作用函数
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
// 响应式依赖钩子
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
// 默认执行一次 getter,用于依赖收集和缓存值
if (cb) {
if (immediate) {
// 立即执行 - 触发一次回调
job()
} else {
// 只获取一次值,触发响应字段 track
oldValue = effect.run()
}
} else if (flush === 'post') {
// watchPostEffect
// 将副作用函数放到 放入 post调度任务队列pendingPostFlushCbs,等待第一次组件更新update后执行
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
// 否则,立即执行一次副作用函数 watchEffect,用于依赖收集
effect.run()
}
// 定义取消watch函数
const unwatch = () => {
// 清除副作用函数
effect.stop() // effect.active = false,同时从将关联的deps中删除此effect
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch // 返回取消watch监听函数
}
整个watch实现的代码可真多,看的真累!!!
简单总结一下:整个doWatch,主要根据第二参数 cb 来区分一些逻辑差异(watchEffect和watch的作用效果)。通过第一个参数source去声明一个getter方法,用于返回监听的响应字段值(watch)/或者执行副作用函数(watchEffect),可以把getter当成是响应字段的副作用函数,所以此getter方法声明了一个对应的副作用函数对象effect,并且默认都会执行一次getter用于响应字段的依赖收集。如果配置immediate
,还需要默认执行一次cb。
根据为watch定义了一个调度方法scheduler<SchedulerJob,根据第二参数cb,区分执行逻辑。如果带cb(watch),需要重新执行getter方法获取监听字段新值,然后触发cb回调执行;不带cb (watchEffect),只需要重新执行副作用函数即可。
通过flush定义,如果是 sync
,当响应字段触发更新通知,直接执行scheduler;如果是 pre
,则在响应字段通知更新后将scheduler加入调度任务队列queue,如果是 post
,则在响应字段通知更新后将scheduler加入调度任务队列pendingPostFlushCbs,加入队列都需要等待队列清空才能执行。
watchsynceffect
watchEffect()
使用flush: 'sync'
选项时的别名。
在执行 doWatch
传入的第三参数 option
默认设置flush: 'sync'
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? { ...options, flush: 'sync' }
: { flush: 'sync' }) as WatchOptionsBase
)
}
watchPostEffect
watchEffect()
使用flush: 'post'
选项时的别名。
在执行 doWatch
传入的第三参数 option
默认设置flush: 'post'
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? { ...options, flush: 'post' }
: { flush: 'post' }) as WatchOptionsBase
)
}
watch
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
watch 类型定义:
function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>, // 监听响应数据
cb: any, // 监听回调
options?: WatchOptions<Immediate> // 配置项
): WatchStopHandle // 取消监听函数
// 监听响应式对象类型
type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
// 继承WatchOptionsBase:{flush?: 'pre'|'post'|'sync'}
interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate // 立即执行一次
deep?: boolean // 深度递归监听
}
watch方法执行也是直接调用 doWatch
,传入第二参数 cb:WatchCallback
:
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
// 第二参数格式校验
if (__DEV__ && !isFunction(cb)) {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`
)
}
return doWatch(source as any, cb, options)
}
this.$watch
// this.$watch
export function instanceWatch(
this: ComponentInternalInstance,
source: string | Function,
value: WatchCallback | ObjectWatchOptionItem,
options?: WatchOptions
): WatchStopHandle {
const publicThis = this.proxy as any // 获取组件实例
const getter = isString(source) // 定义 getter 函数,返回监听响应字段值(副作用函数)
? source.includes('.')
? createPathGetter(publicThis, source) // 解析对象链式路径,返回响应字段值
: () => publicThis[source] // 返回响应字段值
: source.bind(publicThis, publicThis) // 传入getter函数-返回函数执行结果
// 自定义回调函数
let cb
if (isFunction(value)) {
// 如果传入函数,直接作为回调函数
cb = value
} else {
// 如果是对象,获取handler字段作为回调函数
cb = value.handler as Function
options = value
}
// 指定当前组件 this - 设置effect作用域scope为当前组件instance.scope - doWatch需要
// 理解成 记录当前组件的栈
// 这里需要入栈
const cur = currentInstance
setCurrentInstance(this)
// 定义watch
const res = doWatch(getter, cb.bind(publicThis), options)
// 出栈
if (cur) {
setCurrentInstance(cur)
} else {
unsetCurrentInstance()
}
// 返回unwatch方法
return res
}
this.$watch
的实现也依赖 doWatch
,比watch api多了几步,需要解析响应字段和回调。
现在,整个响应式原理相关模块api节本都阅读完了,该做个原理总结了,抛开这些烦人代码,用图解阐述。