Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Jan 12, 2024
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