Skip to content

Commit

Permalink
feat(reactivity): base watch, getCurrentWatcher, and `onWatcherCl…
Browse files Browse the repository at this point in the history
…eanup` (#9927)
  • Loading branch information
LittleSound authored Aug 20, 2024
1 parent 44973bb commit 205e5b5
Show file tree
Hide file tree
Showing 9 changed files with 723 additions and 324 deletions.
196 changes: 196 additions & 0 deletions packages/reactivity/__tests__/watch.spec.ts
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'])
})
})
11 changes: 11 additions & 0 deletions packages/reactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,14 @@ export {
} from './effectScope'
export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
export {
watch,
getCurrentWatcher,
traverse,
onWatcherCleanup,
WatchErrorCodes,
type WatchOptions,
type WatchScheduler,
type WatchStopHandle,
type WatchHandle,
} from './watch'
Loading

0 comments on commit 205e5b5

Please sign in to comment.