一行一行详解Vue3 watch/watchEffect源码
前置知识:响应式基本原理
置知识: vue3响应式系统基本原理得知道吧:响应式对象(reactive, ref),effect, track, trigger这几个是大概什么作用要知道,不知道我简短介绍
const obj = reactive({a: 0})
这个函数会对一个对象作代理(proxy)
对obj任何键的访问都会触发track函数,对obj任何键值更改会触发trigger函数
effect(fn)
用来被trigger触发
那么用法如下:
一个effect里面有我们传入的一个函数,当这个函数去访问obj.a的时候,触发track函数,
const fn = () => {
console.log(obj.a) //访问了obj.a,被track住
}
effect(fn)
obj.a = 1 //这里会触发trigger让上面fn重新调用
track会去查找最"新"的effect,然后保存住 obj<-->a<-->effect这三角恋关系。这个时候如果我更改了它:obj.a = 1,那么trigger会去找obj,a对应的effect是谁,找到之后重新运行这个effect保存的函数,这就是vue3响应式的原理,而里面的细节可就太多了这里就不深入了
前置知识:watch和watchEffect用法
vue3 watchEffect/watch api大家应该了解,就是监听响应式对象,在改变的时候重新执行指定回调
const state = reactive({
star: 0
})
watch(state, (newVal, oldVal) => {
console.log(state.star)
console.log(newVal)
console.log(oldVal)
})
state.star++ //这会再次打印state.star, 新的值,老的值
watchEffect(后面简称we)则是直接,传入回调即可
const state = reactive({
star: 0
})
watch(() => {
console.log(state.star)
})
state.star++ //这会再次打印state.star
一行一行源码解析
我们进入源码看到这两个方法实际上用的同一套api
// Simple effect.
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
// watch
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
return doWatch(source as any, cb, options)
}
可以看到watchEffect调用第二个参数是null,而watch调用第二个参数则是我们传入的回调函数
进入doWatch里面, 以下代码全部在doWatch中
let getter: () => any
let forceTrigger = false
if (isRef(source)) {
getter = () => (source as Ref).value
forceTrigger = !!(source as Ref)._shallow
} else if (isReactive(source)) {
getter = () => source
deep = true
} else if {
...
}
看到首先我们定义了一个getter,getter的作用是去获取要监听的数据,当source是ref或者reactive的时候,getter就是去取对应的数据,getter的实现根据不同的监听目标而不同,
PS:callWithErrorHandling是调用传入的函数,如果出错有相应的处理,就当做
...
else if (isArray(source)) {
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value //如果是ref,则取得其value
} else if (isReactive(s)) {
//这里traverse会递归去获得reactive对象的所有键值,
//因为只要访问一个键就会把这个键依赖收集住,递归把所有键全部都触发依赖收集
return traverse(s)
} else if (isFunction(s)) {
//source也可以接受一个返回ref的函数
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s) //所有情况都不是,dev模式下报个错
}
})
}
...
source可以是一个返回一个响应式对象的值,或者返回一个ref对象的函数如下
const count = ref(0)
watch(() => count, (newCount, oldCount) => {
...
})
所以当source是函数的时候
else if (isFunction(source)) {
if (cb) {
/**进入这里说明是watch而不是watchEffect */
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
/**进入这里说明是watchEffect而不是watch */
getter = () => {
// instance是一个全局变量,可以理解为当前执行这个方法的组件
if (instance && instance.isUnmounted) {
return
}
//cleanup是注册的清理函数
if (cleanup) {
cleanup()
}
return callWithErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
}
}
以上能看到如果是函数,我们的getter其实就是去取得那个值就可以,因为访问响应式对象就会追踪到依赖,其中cleanup函数注册的地方很巧妙
声明cleanup的地方其实就在下面几行
let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
// runner是effect
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
现在可以看到,调用onInvalidate传入一个回调函数fn,就可以把fn注册到当前effect的onStop去,并且也把这个回调传入到了cleanup,那么当我们调用cleanup就可以调用fn了
那么这个onInvalidate我们用户是怎么能用的呢,它其实作为参数传入了watch和watchEffect
// onInvalidate传入这里
watchEffect((onInvalidate) => {
window.addEventListener("click", handler)
onInvalidate(() => {
window.removeEventListener("click", handler)
})
})
这样就将这个清除函数暴露给了我们用户,你一定会想到react的处理方式
useEffect(() => {
window.addEventListener("click", handler)
return () => {
window.removeEventListener("click", handler)
}
})
react的方法看着很直观,那么vue为什么不采用这种写法呢?因为这种写法不支持async await,useEffect里面的函数只能是普通函数,因为async函数或者生成器函数,返回值会用promise包一层,而vue这种是可以直接用async的,当然react想用也可以,在里面做一个IIFE自调用async函数就可以,但是就很丑了
接着回到刚刚的doWatch里面
/**watch api*/
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
如果cb存在,说明是watch而不是watchEffect,那么将这个baseGetter再包一层,因为有可能我们传入的source是一个reactive,要递归监控他所有键,而刚刚没有对这种情况作特殊处理,cleanup上面已经说过了,继续往下看
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
//这里runner是在下面定义的,js是可以在函数里面链接到未声明变量的,
//runner是一个effect传入的函数是上面的getter,也就是说我们调用effect就可以获得getter的返回值
//当这个runner被取消之后,我们什么也不执行直接return
if (!runner.active) {
return
}
if (cb) { //有cb说明我们使用的是watch而不是watchEffect
const newValue = runner() //调用runner获得这次的值
// 判断新旧的值是不是一样的,如果是一样的没有改变就不用处理
if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
// 如果清理函数有,就调用清理函数,防止内存泄露
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 : oldValue,
onInvalidate
])
oldValue = newValue
}
} else {
runner() //对于watchEffect,里面就是传入的函数,直接调用这个runner就行
}
}
聪明的你一看,欸这个watchEffect没有处理cleanup!!!赶紧去提PR,实际上watchEffect是处理了的,可以翻到上面的getter里面可以看到
// no cb -> simple effect
getter = () => {
...
if (cleanup) {
cleanup()
}
...
}
白高兴一场,以为能提个小PR呢
老老实实继续往下看
job.allowRecurse = !!cb
/**allowRecurse就是允许递归调用,也就是说watch是可以在里面修改值,达到重新触发watch的目的
比如说
watch(count, (newVal, oldVal) => {
if (newVal % 2) {
count.value = newVal + 1
}
})*/
let scheduler: ReactiveEffectOptions['scheduler']
if (flush === 'sync') {
/*scheduler是调度器的意思,当effect被trigger触发的时候,会判断有没有调度器,
如果有就会调用这个调度器而不是直接调用effect本身*/
scheduler = job
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
// 如果是pre第一次调用,同步调用一次,这里我猜测是防止打乱渲染,因为渲染的flush就是pre,
// 防止后面mount的时候,触发了这个job,
job()
}
}
}
根据flush来判断更新的时机,sync代表同步更新,我们将调度器直接设置成job就可以,而如果是pre或者post,那么就要放入微任务队列里面去,让事件循环调度它。注意这里第一次调用的时候,根据文档描述,如果是pre,初始化依然是立刻调用
现在调度器已经有了,接下来看effect了
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
// 这个方法将effect和当前的组件“绑定”,就是将effect推入到组件的effect队列里面
recordInstanceBoundEffect(runner, instance)
这里的effect终于将之前提到的getter,scheduler用上了,这里有lazy: true表示这个effect不会立即执行getter,而需要手动去调用
还是回到我们的doWatch继续看
// 初始运行
if (cb) {
/*因为watch默认是惰性的,要改变之后才会触发,如果传入immediate为true就会立即执行调用job*/
if (immediate) {
job()
} else {
/*在这里进行了依赖收集(track),将oldValue也就是传入watch里面的那个旧值记录*/
oldValue = runner()
}
} else if (flush === 'post') {
/*如果不是watch,并且flush是post,应该放在下一个"tick"中执行这个watchEffect
这个方法,就是将effect推入到post队列中,在之后微任务执行会检查post队列,如果有
任务就执行,那个时候就会执行这个effect去追踪依赖*/
queuePostRenderEffect(runner, instance && instance.suspense)
} else {
/*默认的watchEffect我们直接调用这个effect就好了*/
runner()
}
接下来最后一步了,我们知道watchEffect会返回一个函数去停止这个watchEffect,所以最好返回一个函数
return () => {
stop(runner) // 这一步会将effect的active属性改成false,下次调用发现是false的话就不会执行相应回调
if (instance) {
remove(instance.effects!, runner) //remove就是将这个effect从组件的effect队列里面移除
}
}
谢谢你看到这,完整代码如下
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
instance = currentInstance
): WatchStopHandle {
if (__DEV__ && !cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
if (deep !== undefined) {
warn(
`watch() "deep" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
}
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.`
)
}
let getter: () => any
let forceTrigger = false
if (isRef(source)) {
getter = () => (source as Ref).value
forceTrigger = !!(source as Ref)._shallow
} else if (isReactive(source)) {
getter = () => source
deep = true
} else if (isArray(source)) {
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)) {
/**watch api */
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
/**watchEffect api */
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
/**watch api */
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
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) {
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
undefined,
onInvalidate
])
}
return NOOP
}
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
if (!runner.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = runner()
if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
// 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 ? 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']
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()
}
}
}
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
recordInstanceBoundEffect(runner, instance)
// initial run
if (cb) {
if (immediate) {
job()
} else {
oldValue = runner()
}
} else if (flush === 'post') {
queuePostRenderEffect(runner, instance && instance.suspense)
} else {
runner()
}
return () => {
stop(runner)
if (instance) {
remove(instance.effects!, runner)
}
}
}