-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(reactivity): base
watch
, getCurrentWatcher
, and `onWatcherCl…
…eanup` (#9927)
- Loading branch information
1 parent
44973bb
commit 205e5b5
Showing
9 changed files
with
723 additions
and
324 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import { | ||
EffectScope, | ||
type Ref, | ||
WatchErrorCodes, | ||
type WatchOptions, | ||
type WatchScheduler, | ||
onWatcherCleanup, | ||
ref, | ||
watch, | ||
} from '../src' | ||
|
||
const queue: (() => void)[] = [] | ||
|
||
// a simple scheduler for testing purposes | ||
let isFlushPending = false | ||
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any> | ||
const nextTick = (fn?: () => any) => | ||
fn ? resolvedPromise.then(fn) : resolvedPromise | ||
|
||
const scheduler: WatchScheduler = (job, isFirstRun) => { | ||
if (isFirstRun) { | ||
job() | ||
} else { | ||
queue.push(job) | ||
flushJobs() | ||
} | ||
} | ||
|
||
const flushJobs = () => { | ||
if (isFlushPending) return | ||
isFlushPending = true | ||
resolvedPromise.then(() => { | ||
queue.forEach(job => job()) | ||
queue.length = 0 | ||
isFlushPending = false | ||
}) | ||
} | ||
|
||
describe('watch', () => { | ||
test('effect', () => { | ||
let dummy: any | ||
const source = ref(0) | ||
watch(() => { | ||
dummy = source.value | ||
}) | ||
expect(dummy).toBe(0) | ||
source.value++ | ||
expect(dummy).toBe(1) | ||
}) | ||
|
||
test('with callback', () => { | ||
let dummy: any | ||
const source = ref(0) | ||
watch(source, () => { | ||
dummy = source.value | ||
}) | ||
expect(dummy).toBe(undefined) | ||
source.value++ | ||
expect(dummy).toBe(1) | ||
}) | ||
|
||
test('call option with error handling', () => { | ||
const onError = vi.fn() | ||
const call: WatchOptions['call'] = function call(fn, type, args) { | ||
if (Array.isArray(fn)) { | ||
fn.forEach(f => call(f, type, args)) | ||
return | ||
} | ||
try { | ||
fn.apply(null, args) | ||
} catch (e) { | ||
onError(e, type) | ||
} | ||
} | ||
|
||
watch( | ||
() => { | ||
throw 'oops in effect' | ||
}, | ||
null, | ||
{ call }, | ||
) | ||
|
||
const source = ref(0) | ||
const effect = watch( | ||
source, | ||
() => { | ||
onWatcherCleanup(() => { | ||
throw 'oops in cleanup' | ||
}) | ||
throw 'oops in watch' | ||
}, | ||
{ call }, | ||
) | ||
|
||
expect(onError.mock.calls.length).toBe(1) | ||
expect(onError.mock.calls[0]).toMatchObject([ | ||
'oops in effect', | ||
WatchErrorCodes.WATCH_CALLBACK, | ||
]) | ||
|
||
source.value++ | ||
expect(onError.mock.calls.length).toBe(2) | ||
expect(onError.mock.calls[1]).toMatchObject([ | ||
'oops in watch', | ||
WatchErrorCodes.WATCH_CALLBACK, | ||
]) | ||
|
||
effect!.stop() | ||
source.value++ | ||
expect(onError.mock.calls.length).toBe(3) | ||
expect(onError.mock.calls[2]).toMatchObject([ | ||
'oops in cleanup', | ||
WatchErrorCodes.WATCH_CLEANUP, | ||
]) | ||
}) | ||
|
||
test('watch with onWatcherCleanup', async () => { | ||
let dummy = 0 | ||
let source: Ref<number> | ||
const scope = new EffectScope() | ||
|
||
scope.run(() => { | ||
source = ref(0) | ||
watch(onCleanup => { | ||
source.value | ||
|
||
onCleanup(() => (dummy += 2)) | ||
onWatcherCleanup(() => (dummy += 3)) | ||
onWatcherCleanup(() => (dummy += 5)) | ||
}) | ||
}) | ||
expect(dummy).toBe(0) | ||
|
||
scope.run(() => { | ||
source.value++ | ||
}) | ||
expect(dummy).toBe(10) | ||
|
||
scope.run(() => { | ||
source.value++ | ||
}) | ||
expect(dummy).toBe(20) | ||
|
||
scope.stop() | ||
expect(dummy).toBe(30) | ||
}) | ||
|
||
test('nested calls to baseWatch and onWatcherCleanup', async () => { | ||
let calls: string[] = [] | ||
let source: Ref<number> | ||
let copyist: Ref<number> | ||
const scope = new EffectScope() | ||
|
||
scope.run(() => { | ||
source = ref(0) | ||
copyist = ref(0) | ||
// sync by default | ||
watch( | ||
() => { | ||
const current = (copyist.value = source.value) | ||
onWatcherCleanup(() => calls.push(`sync ${current}`)) | ||
}, | ||
null, | ||
{}, | ||
) | ||
// with scheduler | ||
watch( | ||
() => { | ||
const current = copyist.value | ||
onWatcherCleanup(() => calls.push(`post ${current}`)) | ||
}, | ||
null, | ||
{ scheduler }, | ||
) | ||
}) | ||
|
||
await nextTick() | ||
expect(calls).toEqual([]) | ||
|
||
scope.run(() => source.value++) | ||
expect(calls).toEqual(['sync 0']) | ||
await nextTick() | ||
expect(calls).toEqual(['sync 0', 'post 0']) | ||
calls.length = 0 | ||
|
||
scope.run(() => source.value++) | ||
expect(calls).toEqual(['sync 1']) | ||
await nextTick() | ||
expect(calls).toEqual(['sync 1', 'post 1']) | ||
calls.length = 0 | ||
|
||
scope.stop() | ||
expect(calls).toEqual(['sync 2', 'post 2']) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.