Skip to content

Commit 5d65a83

Browse files
committed
feat(runtime): lifecycle beforeUpdate and Updated hooks
1 parent fb4d9a1 commit 5d65a83

File tree

5 files changed

+132
-24
lines changed

5 files changed

+132
-24
lines changed

packages/runtime-vapor/src/component.ts

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface ComponentInternalInstance {
4747
// lifecycle
4848
get isMounted(): boolean
4949
get isUnmounted(): boolean
50+
isUpdating: boolean
5051
isUnmountedRef: Ref<boolean>
5152
isMountedRef: Ref<boolean>
5253
// TODO: registory of provides, lifecycles, ...
@@ -155,6 +156,7 @@ export const createComponentInstance = (
155156
get isUnmounted() {
156157
return isUnmountedRef.value
157158
},
159+
isUpdating: false,
158160
isMountedRef,
159161
isUnmountedRef,
160162
// TODO: registory of provides, appContext, lifecycles, ...

packages/runtime-vapor/src/directive.ts

+14-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { isFunction } from '@vue/shared'
22
import { type ComponentInternalInstance, currentInstance } from './component'
33
import { watchEffect } from './apiWatch'
4+
import { pauseTracking, resetTracking } from '@vue/reactivity'
5+
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
46

57
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
68

@@ -27,7 +29,7 @@ export type DirectiveHookName =
2729
| 'created'
2830
| 'beforeMount'
2931
| 'mounted'
30-
// | 'beforeUpdate'
32+
| 'beforeUpdate'
3133
| 'updated'
3234
| 'beforeUnmount'
3335
| 'unmounted'
@@ -93,12 +95,7 @@ export function withDirectives<T extends Node>(
9395
}
9496
bindings.push(binding)
9597

96-
callDirectiveHook(node, binding, 'created')
97-
98-
watchEffect(() => {
99-
if (!instance.isMountedRef.value) return
100-
callDirectiveHook(node, binding, 'updated')
101-
})
98+
callDirectiveHook(node, binding, instance, 'created')
10299
}
103100

104101
return node
@@ -114,14 +111,15 @@ export function invokeDirectiveHook(
114111
for (const node of nodes) {
115112
const directives = instance.dirs.get(node) || []
116113
for (const binding of directives) {
117-
callDirectiveHook(node, binding, name)
114+
callDirectiveHook(node, binding, instance, name)
118115
}
119116
}
120117
}
121118

122119
function callDirectiveHook(
123120
node: Node,
124121
binding: DirectiveBinding,
122+
instance: ComponentInternalInstance | null,
125123
name: DirectiveHookName,
126124
) {
127125
const { dir } = binding
@@ -133,5 +131,12 @@ function callDirectiveHook(
133131

134132
binding.oldValue = binding.value
135133
binding.value = newValue
136-
hook(node, binding)
134+
// disable tracking inside all lifecycle hooks
135+
// since they can potentially be called inside effects.
136+
pauseTracking()
137+
callWithAsyncErrorHandling(hook, instance, VaporErrorCodes.DIRECTIVE_HOOK, [
138+
node,
139+
binding,
140+
])
141+
resetTracking()
137142
}

packages/runtime-vapor/src/renderWatch.ts

+84-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import {
2-
type BaseWatchErrorCodes,
2+
BaseWatchErrorCodes,
33
type BaseWatchOptions,
44
baseWatch,
55
getCurrentScope,
66
} from '@vue/reactivity'
7-
import { NOOP, remove } from '@vue/shared'
7+
import { NOOP, invokeArrayFns, remove } from '@vue/shared'
88
import { currentInstance } from './component'
9-
import { createVaporRenderingScheduler } from './scheduler'
10-
import { handleError as handleErrorWithInstance } from './errorHandling'
9+
import {
10+
createVaporRenderingScheduler,
11+
queuePostRenderEffect,
12+
} from './scheduler'
13+
import {
14+
callWithAsyncErrorHandling,
15+
handleError as handleErrorWithInstance,
16+
} from './errorHandling'
1117
import { warn } from './warning'
18+
import { invokeDirectiveHook } from './directive'
1219

1320
type WatchStopHandle = () => void
1421

@@ -33,11 +40,31 @@ function doWatch(source: any, cb?: any): WatchStopHandle {
3340
// TODO: SSR
3441
// if (__SSR__) {}
3542

43+
if (__DEV__ && !currentInstance) {
44+
warn(
45+
`${cb ? 'renderWatch' : 'renderEffect'}()` +
46+
' is an internal API and it can only be used inside render()',
47+
)
48+
}
49+
50+
if (cb) {
51+
// watch
52+
cb = wrapEffectCallback(cb)
53+
} else {
54+
// effect
55+
source = wrapEffectCallback(source)
56+
}
57+
3658
const instance =
3759
getCurrentScope() === currentInstance?.scope ? currentInstance : null
3860

39-
extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) =>
61+
extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => {
62+
// callback error handling is in wrapEffectCallback
63+
if (type === BaseWatchErrorCodes.WATCH_CALLBACK) {
64+
throw err
65+
}
4066
handleErrorWithInstance(err, instance, type)
67+
}
4168
extendOptions.scheduler = createVaporRenderingScheduler(instance)
4269

4370
let effect = baseWatch(source, cb, extendOptions)
@@ -53,3 +80,55 @@ function doWatch(source: any, cb?: any): WatchStopHandle {
5380

5481
return unwatch
5582
}
83+
84+
function wrapEffectCallback(callback: (...args: any[]) => any): Function {
85+
const instance = currentInstance!
86+
87+
return (...args: any[]) => {
88+
// with lifecycle
89+
if (instance.isMounted) {
90+
const { bu, u, dirs } = instance
91+
// currentInstance.updating = true
92+
// beforeUpdate hook
93+
const isFirstEffect = !instance.isUpdating
94+
if (isFirstEffect) {
95+
if (bu) {
96+
invokeArrayFns(bu)
97+
}
98+
if (dirs) {
99+
invokeDirectiveHook(instance, 'beforeUpdate')
100+
}
101+
instance.isUpdating = true
102+
}
103+
104+
// run callback
105+
callWithAsyncErrorHandling(
106+
callback,
107+
instance,
108+
BaseWatchErrorCodes.WATCH_CALLBACK,
109+
args,
110+
)
111+
112+
if (isFirstEffect) {
113+
if (dirs) {
114+
queuePostRenderEffect(() => {
115+
instance.isUpdating = false
116+
invokeDirectiveHook(instance, 'updated')
117+
})
118+
}
119+
// updated hook
120+
if (u) {
121+
queuePostRenderEffect(u)
122+
}
123+
}
124+
} else {
125+
// is not mounted
126+
callWithAsyncErrorHandling(
127+
callback,
128+
instance,
129+
BaseWatchErrorCodes.WATCH_CALLBACK,
130+
args,
131+
)
132+
}
133+
}
134+
}

packages/runtime-vapor/src/scheduler.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Scheduler } from '@vue/reactivity'
22
import type { ComponentInternalInstance } from './component'
3+
import { isArray } from '@vue/shared'
34

45
export interface SchedulerJob extends Function {
56
id?: number
@@ -73,15 +74,22 @@ function queueJob(job: SchedulerJob) {
7374
}
7475
}
7576

76-
export function queuePostRenderEffect(cb: SchedulerJob) {
77-
if (
78-
!activePostFlushCbs ||
79-
!activePostFlushCbs.includes(
80-
cb,
81-
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex,
82-
)
83-
) {
84-
pendingPostFlushCbs.push(cb)
77+
export function queuePostRenderEffect(cb: SchedulerJobs) {
78+
if (!isArray(cb)) {
79+
if (
80+
!activePostFlushCbs ||
81+
!activePostFlushCbs.includes(
82+
cb,
83+
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex,
84+
)
85+
) {
86+
pendingPostFlushCbs.push(cb)
87+
}
88+
} else {
89+
// if cb is an array, it is a component lifecycle hook which can only be
90+
// triggered by a job, which is already deduped in the main queue, so
91+
// we can skip duplicate check here to improve perf
92+
pendingPostFlushCbs.push(...cb)
8593
}
8694
queueFlush()
8795
}

playground/src/App.vue

+15-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
onMounted,
66
onBeforeMount,
77
getCurrentInstance,
8+
onBeforeUpdate,
9+
onUpdated,
810
} from 'vue/vapor'
911
1012
const instance = getCurrentInstance()!
@@ -26,12 +28,24 @@ onMounted(() => {
2628
count.value++
2729
}, 1000)
2830
})
31+
32+
onBeforeUpdate(() => {
33+
console.log('before updated')
34+
})
35+
onUpdated(() => {
36+
console.log('updated')
37+
})
38+
39+
const log = (arg: any) => {
40+
console.log('callback in render effect')
41+
return arg
42+
}
2943
</script>
3044

3145
<template>
3246
<div>
3347
<h1 class="red">Counter</h1>
34-
<div>The number is {{ count }}.</div>
48+
<div>The number is {{ log(count) }}.</div>
3549
<div>{{ count }} * 2 = {{ double }}</div>
3650
<div style="display: flex; gap: 8px">
3751
<button @click="inc">inc</button>

0 commit comments

Comments
 (0)