Skip to content

Commit 2cce436

Browse files
authored
feat(runtime-vapor): lifecycle beforeUpdate and updated hooks (#89)
1 parent bb8cc44 commit 2cce436

File tree

10 files changed

+378
-83
lines changed

10 files changed

+378
-83
lines changed

Diff for: packages/reactivity/__tests__/baseWatch.spec.ts

+78
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,82 @@ describe('baseWatch', () => {
175175
scope.stop()
176176
expect(calls).toEqual(['sync 2', 'post 2'])
177177
})
178+
test('baseWatch with middleware', async () => {
179+
let effectCalls: string[] = []
180+
let watchCalls: string[] = []
181+
const source = ref(0)
182+
183+
// effect
184+
baseWatch(
185+
() => {
186+
source.value
187+
effectCalls.push('effect')
188+
onEffectCleanup(() => effectCalls.push('effect cleanup'))
189+
},
190+
null,
191+
{
192+
scheduler,
193+
middleware: next => {
194+
effectCalls.push('before effect running')
195+
next()
196+
effectCalls.push('effect ran')
197+
},
198+
},
199+
)
200+
// watch
201+
baseWatch(
202+
() => source.value,
203+
() => {
204+
watchCalls.push('watch')
205+
onEffectCleanup(() => watchCalls.push('watch cleanup'))
206+
},
207+
{
208+
scheduler,
209+
middleware: next => {
210+
watchCalls.push('before watch running')
211+
next()
212+
watchCalls.push('watch ran')
213+
},
214+
},
215+
)
216+
217+
expect(effectCalls).toEqual([])
218+
expect(watchCalls).toEqual([])
219+
await nextTick()
220+
expect(effectCalls).toEqual([
221+
'before effect running',
222+
'effect',
223+
'effect ran',
224+
])
225+
expect(watchCalls).toEqual([])
226+
effectCalls.length = 0
227+
watchCalls.length = 0
228+
229+
source.value++
230+
await nextTick()
231+
expect(effectCalls).toEqual([
232+
'before effect running',
233+
'effect cleanup',
234+
'effect',
235+
'effect ran',
236+
])
237+
expect(watchCalls).toEqual(['before watch running', 'watch', 'watch ran'])
238+
effectCalls.length = 0
239+
watchCalls.length = 0
240+
241+
source.value++
242+
await nextTick()
243+
expect(effectCalls).toEqual([
244+
'before effect running',
245+
'effect cleanup',
246+
'effect',
247+
'effect ran',
248+
])
249+
expect(watchCalls).toEqual([
250+
'before watch running',
251+
'watch cleanup',
252+
'watch',
253+
'watch ran',
254+
])
255+
})
178256
})

Diff for: packages/reactivity/src/baseWatch.ts

+38-24
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
7171
deep?: boolean
7272
once?: boolean
7373
scheduler?: Scheduler
74+
middleware?: BaseWatchMiddleware
7475
onError?: HandleError
7576
onWarn?: HandleWarn
7677
}
@@ -83,6 +84,7 @@ export type Scheduler = (
8384
effect: ReactiveEffect,
8485
isInit: boolean,
8586
) => void
87+
export type BaseWatchMiddleware = (next: () => unknown) => any
8688
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
8789
export type HandleWarn = (msg: string, ...args: any[]) => void
8890

@@ -132,6 +134,7 @@ export function baseWatch(
132134
scheduler = DEFAULT_SCHEDULER,
133135
onWarn = __DEV__ ? warn : NOOP,
134136
onError = DEFAULT_HANDLE_ERROR,
137+
middleware,
135138
onTrack,
136139
onTrigger,
137140
}: BaseWatchOptions = EMPTY_OBJ,
@@ -211,6 +214,10 @@ export function baseWatch(
211214
activeEffect = currentEffect
212215
}
213216
}
217+
if (middleware) {
218+
const baseGetter = getter
219+
getter = () => middleware(baseGetter)
220+
}
214221
}
215222
} else {
216223
getter = NOOP
@@ -264,31 +271,38 @@ export function baseWatch(
264271
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
265272
: hasChanged(newValue, oldValue))
266273
) {
267-
// cleanup before running cb again
268-
if (cleanup) {
269-
cleanup()
274+
const next = () => {
275+
// cleanup before running cb again
276+
if (cleanup) {
277+
cleanup()
278+
}
279+
const currentEffect = activeEffect
280+
activeEffect = effect
281+
try {
282+
callWithAsyncErrorHandling(
283+
cb!,
284+
onError,
285+
BaseWatchErrorCodes.WATCH_CALLBACK,
286+
[
287+
newValue,
288+
// pass undefined as the old value when it's changed for the first time
289+
oldValue === INITIAL_WATCHER_VALUE
290+
? undefined
291+
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
292+
? []
293+
: oldValue,
294+
onEffectCleanup,
295+
],
296+
)
297+
oldValue = newValue
298+
} finally {
299+
activeEffect = currentEffect
300+
}
270301
}
271-
const currentEffect = activeEffect
272-
activeEffect = effect
273-
try {
274-
callWithAsyncErrorHandling(
275-
cb,
276-
onError,
277-
BaseWatchErrorCodes.WATCH_CALLBACK,
278-
[
279-
newValue,
280-
// pass undefined as the old value when it's changed for the first time
281-
oldValue === INITIAL_WATCHER_VALUE
282-
? undefined
283-
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
284-
? []
285-
: oldValue,
286-
onEffectCleanup,
287-
],
288-
)
289-
oldValue = newValue
290-
} finally {
291-
activeEffect = currentEffect
302+
if (middleware) {
303+
middleware(next)
304+
} else {
305+
next()
292306
}
293307
}
294308
} else {

Diff for: packages/reactivity/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ export {
7676
traverse,
7777
BaseWatchErrorCodes,
7878
type BaseWatchOptions,
79+
type BaseWatchMiddleware,
7980
type Scheduler,
8081
} from './baseWatch'

Diff for: packages/runtime-vapor/__tests__/renderWatch.spec.ts

+121-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { defineComponent } from 'vue'
22
import {
33
nextTick,
4+
onBeforeUpdate,
45
onEffectCleanup,
6+
onUpdated,
57
ref,
68
render,
79
renderEffect,
@@ -25,6 +27,27 @@ beforeEach(() => {
2527
afterEach(() => {
2628
host.remove()
2729
})
30+
const createDemo = (
31+
setupFn: (porps: any, ctx: any) => any,
32+
renderFn: (ctx: any) => any,
33+
) => {
34+
const demo = defineComponent({
35+
setup(...args) {
36+
const returned = setupFn(...args)
37+
Object.defineProperty(returned, '__isScriptSetup', {
38+
enumerable: false,
39+
value: true,
40+
})
41+
return returned
42+
},
43+
})
44+
demo.render = (ctx: any) => {
45+
const t0 = template('<div></div>')
46+
renderFn(ctx)
47+
return t0()
48+
}
49+
return () => render(demo as any, {}, '#host')
50+
}
2851

2952
describe('renderWatch', () => {
3053
test('effect', async () => {
@@ -53,16 +76,26 @@ describe('renderWatch', () => {
5376
expect(dummy).toBe(1)
5477
})
5578

56-
test('scheduling order', async () => {
79+
test('should run with the scheduling order', async () => {
5780
const calls: string[] = []
5881

59-
const demo = defineComponent({
60-
setup() {
82+
const mount = createDemo(
83+
() => {
84+
// setup
6185
const source = ref(0)
6286
const renderSource = ref(0)
6387
const change = () => source.value++
6488
const changeRender = () => renderSource.value++
6589

90+
// Life Cycle Hooks
91+
onUpdated(() => {
92+
calls.push(`updated ${source.value}`)
93+
})
94+
onBeforeUpdate(() => {
95+
calls.push(`beforeUpdate ${source.value}`)
96+
})
97+
98+
// Watch API
6699
watchPostEffect(() => {
67100
const current = source.value
68101
calls.push(`post ${current}`)
@@ -78,33 +111,28 @@ describe('renderWatch', () => {
78111
calls.push(`sync ${current}`)
79112
onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
80113
})
81-
const __returned__ = { source, change, renderSource, changeRender }
82-
Object.defineProperty(__returned__, '__isScriptSetup', {
83-
enumerable: false,
84-
value: true,
114+
return { source, change, renderSource, changeRender }
115+
},
116+
// render
117+
(_ctx) => {
118+
// Render Watch API
119+
renderEffect(() => {
120+
const current = _ctx.renderSource
121+
calls.push(`renderEffect ${current}`)
122+
onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
85123
})
86-
return __returned__
124+
renderWatch(
125+
() => _ctx.renderSource,
126+
(value) => {
127+
calls.push(`renderWatch ${value}`)
128+
onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`))
129+
},
130+
)
87131
},
88-
})
132+
)
89133

90-
demo.render = (_ctx: any) => {
91-
const t0 = template('<div></div>')
92-
renderEffect(() => {
93-
const current = _ctx.renderSource
94-
calls.push(`renderEffect ${current}`)
95-
onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
96-
})
97-
renderWatch(
98-
() => _ctx.renderSource,
99-
(value) => {
100-
calls.push(`renderWatch ${value}`)
101-
onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`))
102-
},
103-
)
104-
return t0()
105-
}
106-
107-
const instance = render(demo as any, {}, '#host')
134+
// Mount
135+
const instance = mount()
108136
const { change, changeRender } = instance.setupState as any
109137

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

145+
// Update
117146
changeRender()
118147
change()
148+
119149
expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
120150
calls.length = 0
121151

122152
await nextTick()
123153
expect(calls).toEqual([
124154
'pre cleanup 0',
125155
'pre 1',
156+
'beforeUpdate 1',
126157
'renderEffect cleanup 0',
127158
'renderEffect 1',
128159
'renderWatch 1',
129160
'post cleanup 0',
130161
'post 1',
162+
'updated 1',
131163
])
132164
})
165+
166+
test('errors should include the execution location with beforeUpdate hook', async () => {
167+
const mount = createDemo(
168+
// setup
169+
() => {
170+
const source = ref()
171+
const update = () => source.value++
172+
onBeforeUpdate(() => {
173+
throw 'error in beforeUpdate'
174+
})
175+
return { source, update }
176+
},
177+
// render
178+
(ctx) => {
179+
renderEffect(() => {
180+
ctx.source
181+
})
182+
},
183+
)
184+
185+
const instance = mount()
186+
const { update } = instance.setupState as any
187+
await expect(async () => {
188+
update()
189+
await nextTick()
190+
}).rejects.toThrow('error in beforeUpdate')
191+
192+
expect(
193+
'[Vue warn] Unhandled error during execution of beforeUpdate hook',
194+
).toHaveBeenWarned()
195+
})
196+
197+
test('errors should include the execution location with updated hook', async () => {
198+
const mount = createDemo(
199+
// setup
200+
() => {
201+
const source = ref(0)
202+
const update = () => source.value++
203+
onUpdated(() => {
204+
throw 'error in updated'
205+
})
206+
return { source, update }
207+
},
208+
// render
209+
(ctx) => {
210+
renderEffect(() => {
211+
ctx.source
212+
})
213+
},
214+
)
215+
216+
const instance = mount()
217+
const { update } = instance.setupState as any
218+
await expect(async () => {
219+
update()
220+
await nextTick()
221+
}).rejects.toThrow('error in updated')
222+
223+
expect(
224+
'[Vue warn] Unhandled error during execution of updated',
225+
).toHaveBeenWarned()
226+
})
133227
})

0 commit comments

Comments
 (0)