refactor(baseWatch): rename onEffectCleanup to onWatcherCleanup and getCurrentEffect to getCurrentWatcher for clarity

This commit is contained in:
Rizumu Ayaka 2024-03-17 22:39:11 +08:00
parent db4040d13a
commit 46761880e9
No known key found for this signature in database
10 changed files with 121 additions and 81 deletions

View File

@ -5,7 +5,7 @@ import {
type SchedulerJob, type SchedulerJob,
type WatchScheduler, type WatchScheduler,
baseWatch, baseWatch,
onEffectCleanup, onWatcherCleanup,
ref, ref,
} from '../src' } from '../src'
@ -72,7 +72,7 @@ describe('baseWatch', () => {
const effect = baseWatch( const effect = baseWatch(
source, source,
() => { () => {
onEffectCleanup(() => { onWatcherCleanup(() => {
throw 'oops in cleanup' throw 'oops in cleanup'
}) })
throw 'oops in watch' throw 'oops in watch'
@ -102,7 +102,7 @@ describe('baseWatch', () => {
]) ])
}) })
test('baseWatch with onEffectCleanup', async () => { test('baseWatch with onWatcherCleanup', async () => {
let dummy = 0 let dummy = 0
let source: Ref<number> let source: Ref<number>
const scope = new EffectScope() const scope = new EffectScope()
@ -113,8 +113,8 @@ describe('baseWatch', () => {
source.value source.value
onCleanup(() => (dummy += 2)) onCleanup(() => (dummy += 2))
onEffectCleanup(() => (dummy += 3)) onWatcherCleanup(() => (dummy += 3))
onEffectCleanup(() => (dummy += 5)) onWatcherCleanup(() => (dummy += 5))
}) })
}) })
expect(dummy).toBe(0) expect(dummy).toBe(0)
@ -133,7 +133,7 @@ describe('baseWatch', () => {
expect(dummy).toBe(30) expect(dummy).toBe(30)
}) })
test('nested calls to baseWatch and onEffectCleanup', async () => { test('nested calls to baseWatch and onWatcherCleanup', async () => {
let calls: string[] = [] let calls: string[] = []
let source: Ref<number> let source: Ref<number>
let copyist: Ref<number> let copyist: Ref<number>
@ -146,7 +146,7 @@ describe('baseWatch', () => {
baseWatch( baseWatch(
() => { () => {
const current = (copyist.value = source.value) const current = (copyist.value = source.value)
onEffectCleanup(() => calls.push(`sync ${current}`)) onWatcherCleanup(() => calls.push(`sync ${current}`))
}, },
null, null,
{}, {},
@ -155,7 +155,7 @@ describe('baseWatch', () => {
baseWatch( baseWatch(
() => { () => {
const current = copyist.value const current = copyist.value
onEffectCleanup(() => calls.push(`post ${current}`)) onWatcherCleanup(() => calls.push(`post ${current}`))
}, },
null, null,
{ scheduler }, { scheduler },
@ -180,6 +180,7 @@ describe('baseWatch', () => {
scope.stop() scope.stop()
expect(calls).toEqual(['sync 2', 'post 2']) expect(calls).toEqual(['sync 2', 'post 2'])
}) })
test('baseWatch with middleware', async () => { test('baseWatch with middleware', async () => {
let effectCalls: string[] = [] let effectCalls: string[] = []
let watchCalls: string[] = [] let watchCalls: string[] = []
@ -190,7 +191,7 @@ describe('baseWatch', () => {
() => { () => {
source.value source.value
effectCalls.push('effect') effectCalls.push('effect')
onEffectCleanup(() => effectCalls.push('effect cleanup')) onWatcherCleanup(() => effectCalls.push('effect cleanup'))
}, },
null, null,
{ {
@ -207,7 +208,7 @@ describe('baseWatch', () => {
() => source.value, () => source.value,
() => { () => {
watchCalls.push('watch') watchCalls.push('watch')
onEffectCleanup(() => watchCalls.push('watch cleanup')) onWatcherCleanup(() => watchCalls.push('watch cleanup'))
}, },
{ {
scheduler, scheduler,

View File

@ -15,7 +15,7 @@ import type { ComputedRef } from './computed'
import { ReactiveFlags } from './constants' import { ReactiveFlags } from './constants'
import { import {
type DebuggerOptions, type DebuggerOptions,
type EffectScheduler, EffectFlags,
ReactiveEffect, ReactiveEffect,
pauseTracking, pauseTracking,
resetTracking, resetTracking,
@ -46,7 +46,7 @@ export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
immediate?: Immediate immediate?: Immediate
deep?: boolean deep?: boolean
once?: boolean once?: boolean
scheduler?: Scheduler scheduler?: WatchScheduler
middleware?: BaseWatchMiddleware middleware?: BaseWatchMiddleware
onError?: HandleError onError?: HandleError
onWarn?: HandleWarn onWarn?: HandleWarn
@ -55,22 +55,41 @@ export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
// initial value for watchers to trigger on undefined initial values // initial value for watchers to trigger on undefined initial values
const INITIAL_WATCHER_VALUE = {} const INITIAL_WATCHER_VALUE = {}
export type Scheduler = ( export type WatchScheduler = (
job: SchedulerJob, job: SchedulerJob,
effect: ReactiveEffect, effect: ReactiveEffect,
isInit: boolean, immediateFirstRun: boolean,
hasCb: boolean,
) => void ) => void
export type BaseWatchMiddleware = (next: () => unknown) => any export type BaseWatchMiddleware = (next: () => unknown) => any
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
export type HandleWarn = (msg: string, ...args: any[]) => void export type HandleWarn = (msg: string, ...args: any[]) => void
const DEFAULT_SCHEDULER: Scheduler = job => job() const DEFAULT_SCHEDULER: WatchScheduler = (
job,
effect,
immediateFirstRun,
hasCb,
) => {
if (immediateFirstRun) {
!hasCb && effect.run()
} else {
job()
}
}
const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => { const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => {
throw err throw err
} }
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap() const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
let activeEffect: ReactiveEffect | undefined = undefined let activeWatcher: ReactiveEffect | undefined = undefined
/**
* Returns the current active effect if there is one.
*/
export function getCurrentWatcher() {
return activeWatcher
}
/** /**
* Registers a cleanup callback on the current active effect. This * Registers a cleanup callback on the current active effect. This
@ -79,15 +98,15 @@ let activeEffect: ReactiveEffect | undefined = undefined
* *
* @param cleanupFn - The callback function to attach to the effect's cleanup. * @param cleanupFn - The callback function to attach to the effect's cleanup.
*/ */
export function onEffectCleanup(cleanupFn: () => void) { export function onWatcherCleanup(cleanupFn: () => void, failSilently = false) {
if (activeEffect) { if (activeWatcher) {
const cleanups = const cleanups =
cleanupMap.get(activeEffect) || cleanupMap.get(activeWatcher) ||
cleanupMap.set(activeEffect, []).get(activeEffect)! cleanupMap.set(activeWatcher, []).get(activeWatcher)!
cleanups.push(cleanupFn) cleanups.push(cleanupFn)
} else if (__DEV__) { } else if (__DEV__ && !failSilently) {
warn( warn(
`onEffectCleanup() was called when there was no active effect` + `onWatcherCleanup() was called when there was no active watcher` +
` to associate with.`, ` to associate with.`,
) )
} }
@ -170,17 +189,17 @@ export function baseWatch(
resetTracking() resetTracking()
} }
} }
const currentEffect = activeEffect const currentEffect = activeWatcher
activeEffect = effect activeWatcher = effect
try { try {
return callWithAsyncErrorHandling( return callWithAsyncErrorHandling(
source, source,
onError, onError,
BaseWatchErrorCodes.WATCH_CALLBACK, BaseWatchErrorCodes.WATCH_CALLBACK,
[onEffectCleanup], [onWatcherCleanup],
) )
} finally { } finally {
activeEffect = currentEffect activeWatcher = currentEffect
} }
} }
if (middleware) { if (middleware) {
@ -198,31 +217,20 @@ export function baseWatch(
getter = () => traverse(baseGetter()) getter = () => traverse(baseGetter())
} }
const scope = getCurrentScope()
if (once) { if (once) {
if (!cb) { if (cb) {
// onEffectCleanup need use effect as a key
scope?.effects.push((effect = {} as any))
getter()
return
}
if (immediate) {
// onEffectCleanup need use effect as a key
scope?.effects.push((effect = {} as any))
callWithAsyncErrorHandling(
cb,
onError,
BaseWatchErrorCodes.WATCH_CALLBACK,
[getter(), isMultiSource ? [] : undefined, onEffectCleanup],
)
return
}
const _cb = cb const _cb = cb
cb = (...args) => { cb = (...args) => {
_cb(...args) _cb(...args)
effect?.stop() effect?.stop()
} }
} else {
const _getter = getter
getter = () => {
_getter()
effect?.stop()
}
}
} }
let oldValue: any = isMultiSource let oldValue: any = isMultiSource
@ -250,8 +258,8 @@ export function baseWatch(
if (cleanup) { if (cleanup) {
cleanup() cleanup()
} }
const currentEffect = activeEffect const currentWatcher = activeWatcher
activeEffect = effect activeWatcher = effect
try { try {
callWithAsyncErrorHandling( callWithAsyncErrorHandling(
cb!, cb!,
@ -265,12 +273,12 @@ export function baseWatch(
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? [] ? []
: oldValue, : oldValue,
onEffectCleanup, onWatcherCleanup,
], ],
) )
oldValue = newValue oldValue = newValue
} finally { } finally {
activeEffect = currentEffect activeWatcher = currentWatcher
} }
} }
if (middleware) { if (middleware) {

View File

@ -80,11 +80,12 @@ export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants' export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
export { export {
baseWatch, baseWatch,
onEffectCleanup, getCurrentWatcher,
traverse, traverse,
onWatcherCleanup,
BaseWatchErrorCodes, BaseWatchErrorCodes,
type BaseWatchOptions, type BaseWatchOptions,
type BaseWatchMiddleware, type BaseWatchMiddleware,
type Scheduler, type WatchScheduler,
type SchedulerJob,
} from './baseWatch' } from './baseWatch'
export { type SchedulerJob, SchedulerJobFlags } from './scheduler'

View File

@ -0,0 +1,30 @@
export enum SchedulerJobFlags {
QUEUED = 1 << 0,
PRE = 1 << 1,
/**
* Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler.
*
* By default, a job cannot trigger itself because some built-in method calls,
* e.g. Array.prototype.push actually performs reads as well (#1740) which
* can lead to confusing infinite loops.
* The allowed cases are component update functions and watch callbacks.
* Component update functions may update child component props, which in turn
* trigger flush: "pre" watch callbacks that mutates state that the parent
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
* triggers itself again, it's likely intentional and it is the user's
* responsibility to perform recursive state mutation that eventually
* stabilizes (#1727).
*/
ALLOW_RECURSE = 1 << 2,
DISPOSED = 1 << 3,
}
export interface SchedulerJob extends Function {
id?: number
/**
* flags can technically be undefined, but it can still be used in bitwise
* operations just like 0.
*/
flags?: SchedulerJobFlags
}

View File

@ -5,7 +5,7 @@ import {
defineComponent, defineComponent,
getCurrentInstance, getCurrentInstance,
nextTick, nextTick,
onEffectCleanup, onWatcherCleanup,
reactive, reactive,
ref, ref,
watch, watch,
@ -395,17 +395,17 @@ describe('api: watch', () => {
expect(cleanup).toHaveBeenCalledTimes(2) expect(cleanup).toHaveBeenCalledTimes(2)
}) })
it('onEffectCleanup', async () => { it('onWatcherCleanup', async () => {
const count = ref(0) const count = ref(0)
const cleanupEffect = vi.fn() const cleanupEffect = vi.fn()
const cleanupWatch = vi.fn() const cleanupWatch = vi.fn()
const stopEffect = watchEffect(() => { const stopEffect = watchEffect(() => {
onEffectCleanup(cleanupEffect) onWatcherCleanup(cleanupEffect)
count.value count.value
}) })
const stopWatch = watch(count, () => { const stopWatch = watch(count, () => {
onEffectCleanup(cleanupWatch) onWatcherCleanup(cleanupWatch)
}) })
count.value++ count.value++

View File

@ -28,7 +28,7 @@ export {
// effect // effect
effect, effect,
stop, stop,
onEffectCleanup, onWatcherCleanup,
ReactiveEffect, ReactiveEffect,
// effect scope // effect scope
effectScope, effectScope,

View File

@ -2,13 +2,13 @@ import type { Ref } from '@vue/reactivity'
import { import {
EffectScope, EffectScope,
nextTick, nextTick,
onEffectCleanup, onWatcherCleanup,
ref, ref,
watchEffect, watchEffect,
watchSyncEffect, watchSyncEffect,
} from '../src' } from '../src'
describe('watchEffect and onEffectCleanup', () => { describe('watchEffect and onWatcherCleanup', () => {
test('basic', async () => { test('basic', async () => {
let dummy = 0 let dummy = 0
let source: Ref<number> let source: Ref<number>
@ -20,8 +20,8 @@ describe('watchEffect and onEffectCleanup', () => {
source.value source.value
onCleanup(() => (dummy += 2)) onCleanup(() => (dummy += 2))
onEffectCleanup(() => (dummy += 3)) onWatcherCleanup(() => (dummy += 3))
onEffectCleanup(() => (dummy += 5)) onWatcherCleanup(() => (dummy += 5))
}) })
}) })
await nextTick() await nextTick()
@ -55,11 +55,11 @@ describe('watchEffect and onEffectCleanup', () => {
double = ref(0) double = ref(0)
watchEffect(() => { watchEffect(() => {
double.value = source.value * 2 double.value = source.value * 2
onEffectCleanup(() => (dummy += 2)) onWatcherCleanup(() => (dummy += 2))
}) })
watchSyncEffect(() => { watchSyncEffect(() => {
double.value double.value
onEffectCleanup(() => (dummy += 3)) onWatcherCleanup(() => (dummy += 3))
}) })
}) })
await nextTick() await nextTick()

View File

@ -1,8 +1,8 @@
import { import {
nextTick, nextTick,
onBeforeUpdate, onBeforeUpdate,
onEffectCleanup,
onUpdated, onUpdated,
onWatcherCleanup,
ref, ref,
renderEffect, renderEffect,
renderWatch, renderWatch,
@ -101,17 +101,17 @@ describe('renderWatch', () => {
watchPostEffect(() => { watchPostEffect(() => {
const current = source.value const current = source.value
calls.push(`post ${current}`) calls.push(`post ${current}`)
onEffectCleanup(() => calls.push(`post cleanup ${current}`)) onWatcherCleanup(() => calls.push(`post cleanup ${current}`))
}) })
watchEffect(() => { watchEffect(() => {
const current = source.value const current = source.value
calls.push(`pre ${current}`) calls.push(`pre ${current}`)
onEffectCleanup(() => calls.push(`pre cleanup ${current}`)) onWatcherCleanup(() => calls.push(`pre cleanup ${current}`))
}) })
watchSyncEffect(() => { watchSyncEffect(() => {
const current = source.value const current = source.value
calls.push(`sync ${current}`) calls.push(`sync ${current}`)
onEffectCleanup(() => calls.push(`sync cleanup ${current}`)) onWatcherCleanup(() => calls.push(`sync cleanup ${current}`))
}) })
return { source, change, renderSource, changeRender } return { source, change, renderSource, changeRender }
}, },
@ -121,13 +121,13 @@ describe('renderWatch', () => {
renderEffect(() => { renderEffect(() => {
const current = _ctx.renderSource const current = _ctx.renderSource
calls.push(`renderEffect ${current}`) calls.push(`renderEffect ${current}`)
onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`)) onWatcherCleanup(() => calls.push(`renderEffect cleanup ${current}`))
}) })
renderWatch( renderWatch(
() => _ctx.renderSource, () => _ctx.renderSource,
value => { value => {
calls.push(`renderWatch ${value}`) calls.push(`renderWatch ${value}`)
onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`)) onWatcherCleanup(() => calls.push(`renderWatch cleanup ${value}`))
}, },
) )
}, },

View File

@ -29,7 +29,7 @@ export {
// effect // effect
stop, stop,
ReactiveEffect, ReactiveEffect,
onEffectCleanup, onWatcherCleanup,
// effect scope // effect scope
effectScope, effectScope,
EffectScope, EffectScope,

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
onEffectCleanup, onWatcherCleanup,
ref, ref,
watch, watch,
watchEffect, watchEffect,
@ -14,30 +14,30 @@ const add = () => source.value++
watchPostEffect(() => { watchPostEffect(() => {
const current = source.value const current = source.value
console.log('post', current) console.log('post', current)
onEffectCleanup(() => console.log('cleanup post', current)) onWatcherCleanup(() => console.log('cleanup post', current))
}) })
watchEffect(() => { watchEffect(() => {
const current = source.value const current = source.value
console.log('pre', current) console.log('pre', current)
onEffectCleanup(() => console.log('cleanup pre', current)) onWatcherCleanup(() => console.log('cleanup pre', current))
}) })
watchSyncEffect(() => { watchSyncEffect(() => {
const current = source.value const current = source.value
console.log('sync', current) console.log('sync', current)
onEffectCleanup(() => console.log('cleanup sync', current)) onWatcherCleanup(() => console.log('cleanup sync', current))
}) })
watch(source, (value, oldValue) => { watch(source, (value, oldValue) => {
console.log('sync watch', value, 'oldValue:', oldValue) console.log('watch', value, 'oldValue:', oldValue)
onEffectCleanup(() => console.log('cleanup sync watch', value)) onWatcherCleanup(() => console.log('cleanup watch', value))
}) })
const onUpdate = (arg: any) => { const onUpdate = (arg: any) => {
const current = source.value const current = source.value
console.log('render', current) console.log('render', current)
onEffectCleanup(() => console.log('cleanup render', current)) onWatcherCleanup(() => console.log('cleanup render', current))
return arg return arg
} }
</script> </script>