Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

feat(runtime-vapor): lifecycle beforeUpdate and updated hooks #89

Merged
merged 11 commits into from
Jan 12, 2024
Merged
78 changes: 78 additions & 0 deletions packages/reactivity/__tests__/baseWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,82 @@ describe('baseWatch', () => {
scope.stop()
expect(calls).toEqual(['sync 2', 'post 2'])
})
test('baseWatch with middleware', async () => {
let effectCalls: string[] = []
let watchCalls: string[] = []
const source = ref(0)

// effect
baseWatch(
() => {
source.value
effectCalls.push('effect')
onEffectCleanup(() => effectCalls.push('effect cleanup'))
},
null,
{
scheduler,
middleware: next => {
effectCalls.push('before effect running')
next()
effectCalls.push('effect ran')
},
},
)
// watch
baseWatch(
() => source.value,
() => {
watchCalls.push('watch')
onEffectCleanup(() => watchCalls.push('watch cleanup'))
},
{
scheduler,
middleware: next => {
watchCalls.push('before watch running')
next()
watchCalls.push('watch ran')
},
},
)

expect(effectCalls).toEqual([])
expect(watchCalls).toEqual([])
await nextTick()
expect(effectCalls).toEqual([
'before effect running',
'effect',
'effect ran',
])
expect(watchCalls).toEqual([])
effectCalls.length = 0
watchCalls.length = 0

source.value++
await nextTick()
expect(effectCalls).toEqual([
'before effect running',
'effect cleanup',
'effect',
'effect ran',
])
expect(watchCalls).toEqual(['before watch running', 'watch', 'watch ran'])
effectCalls.length = 0
watchCalls.length = 0

source.value++
await nextTick()
expect(effectCalls).toEqual([
'before effect running',
'effect cleanup',
'effect',
'effect ran',
])
expect(watchCalls).toEqual([
'before watch running',
'watch cleanup',
'watch',
'watch ran',
])
})
})
62 changes: 38 additions & 24 deletions packages/reactivity/src/baseWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
deep?: boolean
once?: boolean
scheduler?: Scheduler
middleware?: BaseWatchMiddleware
onError?: HandleError
onWarn?: HandleWarn
}
Expand All @@ -83,6 +84,7 @@ export type Scheduler = (
effect: ReactiveEffect,
isInit: boolean,
) => void
export type BaseWatchMiddleware = (next: () => unknown) => any
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
export type HandleWarn = (msg: string, ...args: any[]) => void

Expand Down Expand Up @@ -132,6 +134,7 @@ export function baseWatch(
scheduler = DEFAULT_SCHEDULER,
onWarn = __DEV__ ? warn : NOOP,
onError = DEFAULT_HANDLE_ERROR,
middleware,
onTrack,
onTrigger,
}: BaseWatchOptions = EMPTY_OBJ,
Expand Down Expand Up @@ -211,6 +214,10 @@ export function baseWatch(
activeEffect = currentEffect
}
}
if (middleware) {
const baseGetter = getter
getter = () => middleware(baseGetter)
}
}
} else {
getter = NOOP
Expand Down Expand Up @@ -264,31 +271,38 @@ export function baseWatch(
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
const next = () => {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
const currentEffect = activeEffect
activeEffect = effect
try {
callWithAsyncErrorHandling(
cb!,
onError,
BaseWatchErrorCodes.WATCH_CALLBACK,
[
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onEffectCleanup,
],
)
oldValue = newValue
} finally {
activeEffect = currentEffect
}
}
const currentEffect = activeEffect
activeEffect = effect
try {
callWithAsyncErrorHandling(
cb,
onError,
BaseWatchErrorCodes.WATCH_CALLBACK,
[
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onEffectCleanup,
],
)
oldValue = newValue
} finally {
activeEffect = currentEffect
if (middleware) {
middleware(next)
} else {
next()
}
}
} else {
Expand Down
1 change: 1 addition & 0 deletions packages/reactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,6 @@ export {
traverse,
BaseWatchErrorCodes,
type BaseWatchOptions,
type BaseWatchMiddleware,
type Scheduler,
} from './baseWatch'
148 changes: 121 additions & 27 deletions packages/runtime-vapor/__tests__/renderWatch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { defineComponent } from 'vue'
import {
nextTick,
onBeforeUpdate,
onEffectCleanup,
onUpdated,
ref,
render,
renderEffect,
Expand All @@ -25,6 +27,27 @@ beforeEach(() => {
afterEach(() => {
host.remove()
})
const createDemo = (
setupFn: (porps: any, ctx: any) => any,
renderFn: (ctx: any) => any,
) => {
const demo = defineComponent({
setup(...args) {
const returned = setupFn(...args)
Object.defineProperty(returned, '__isScriptSetup', {
enumerable: false,
value: true,
})
return returned
},
})
demo.render = (ctx: any) => {
const t0 = template('<div></div>')
renderFn(ctx)
return t0()
}
return () => render(demo as any, {}, '#host')
}

describe('renderWatch', () => {
test('effect', async () => {
Expand Down Expand Up @@ -53,16 +76,26 @@ describe('renderWatch', () => {
expect(dummy).toBe(1)
})

test('scheduling order', async () => {
test('should run with the scheduling order', async () => {
const calls: string[] = []

const demo = defineComponent({
setup() {
const mount = createDemo(
() => {
// setup
const source = ref(0)
const renderSource = ref(0)
const change = () => source.value++
const changeRender = () => renderSource.value++

// Life Cycle Hooks
onUpdated(() => {
calls.push(`updated ${source.value}`)
})
onBeforeUpdate(() => {
calls.push(`beforeUpdate ${source.value}`)
})

// Watch API
watchPostEffect(() => {
const current = source.value
calls.push(`post ${current}`)
Expand All @@ -78,33 +111,28 @@ describe('renderWatch', () => {
calls.push(`sync ${current}`)
onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
})
const __returned__ = { source, change, renderSource, changeRender }
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
return { source, change, renderSource, changeRender }
},
// render
(_ctx) => {
// Render Watch API
renderEffect(() => {
const current = _ctx.renderSource
calls.push(`renderEffect ${current}`)
onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
})
return __returned__
renderWatch(
() => _ctx.renderSource,
(value) => {
calls.push(`renderWatch ${value}`)
onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`))
},
)
},
})
)

demo.render = (_ctx: any) => {
const t0 = template('<div></div>')
renderEffect(() => {
const current = _ctx.renderSource
calls.push(`renderEffect ${current}`)
onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
})
renderWatch(
() => _ctx.renderSource,
(value) => {
calls.push(`renderWatch ${value}`)
onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`))
},
)
return t0()
}

const instance = render(demo as any, {}, '#host')
// Mount
const instance = mount()
const { change, changeRender } = instance.setupState as any

expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0'])
Expand All @@ -114,20 +142,86 @@ describe('renderWatch', () => {
expect(calls).toEqual(['post 0'])
calls.length = 0

// Update
changeRender()
change()

expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
calls.length = 0

await nextTick()
expect(calls).toEqual([
'pre cleanup 0',
'pre 1',
'beforeUpdate 1',
'renderEffect cleanup 0',
'renderEffect 1',
'renderWatch 1',
'post cleanup 0',
'post 1',
'updated 1',
])
})

test('errors should include the execution location with beforeUpdate hook', async () => {
const mount = createDemo(
// setup
() => {
const source = ref()
const update = () => source.value++
onBeforeUpdate(() => {
throw 'error in beforeUpdate'
})
return { source, update }
},
// render
(ctx) => {
renderEffect(() => {
ctx.source
})
},
)

const instance = mount()
const { update } = instance.setupState as any
await expect(async () => {
update()
await nextTick()
}).rejects.toThrow('error in beforeUpdate')

expect(
'[Vue warn] Unhandled error during execution of beforeUpdate hook',
).toHaveBeenWarned()
})

test('errors should include the execution location with updated hook', async () => {
const mount = createDemo(
// setup
() => {
const source = ref(0)
const update = () => source.value++
onUpdated(() => {
throw 'error in updated'
})
return { source, update }
},
// render
(ctx) => {
renderEffect(() => {
ctx.source
})
},
)

const instance = mount()
const { update } = instance.setupState as any
await expect(async () => {
update()
await nextTick()
}).rejects.toThrow('error in updated')

expect(
'[Vue warn] Unhandled error during execution of updated',
).toHaveBeenWarned()
})
})
Loading