diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts
index ef858a74a..26e02fc9f 100644
--- a/packages/reactivity/src/effectScope.ts
+++ b/packages/reactivity/src/effectScope.ts
@@ -34,13 +34,13 @@ export class EffectScope {
*/
private index: number | undefined
- constructor(public detached = false) {
- this.parent = activeEffectScope
- if (!detached && activeEffectScope) {
- this.index =
- (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
- this,
- ) - 1
+ constructor(
+ public detached = false,
+ parent = activeEffectScope,
+ ) {
+ this.parent = parent
+ if (!detached && parent) {
+ this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1
}
}
diff --git a/packages/runtime-vapor/__tests__/directives/vShow.spec.ts b/packages/runtime-vapor/__tests__/directives/vShow.spec.ts
index 8f627b975..75c44f8c7 100644
--- a/packages/runtime-vapor/__tests__/directives/vShow.spec.ts
+++ b/packages/runtime-vapor/__tests__/directives/vShow.spec.ts
@@ -77,7 +77,7 @@ describe('directive: v-show', () => {
}).render()
expect(host.innerHTML).toBe('
child
')
- expect(instance.dirs.get(n0)![0].dir).toBe(vShow)
+ expect(instance.scope.dirs!.get(n0)![0].dir).toBe(vShow)
const btn = host.querySelector('button')
btn?.click()
diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts
index e95f28851..fba012c62 100644
--- a/packages/runtime-vapor/__tests__/for.spec.ts
+++ b/packages/runtime-vapor/__tests__/for.spec.ts
@@ -1,5 +1,16 @@
-import { createFor, nextTick, ref, renderEffect } from '../src'
+import { NOOP } from '@vue/shared'
+import {
+ type Directive,
+ children,
+ createFor,
+ nextTick,
+ ref,
+ renderEffect,
+ template,
+ withDirectives,
+} from '../src'
import { makeRender } from './_utils'
+import { unmountComponent } from '../src/apiRender'
const define = makeRender()
@@ -184,4 +195,92 @@ describe('createFor', () => {
await nextTick()
expect(host.innerHTML).toBe('')
})
+
+ test('should work with directive hooks', async () => {
+ const calls: string[] = []
+ const list = ref([0])
+ const update = ref(0)
+ const add = () => list.value.push(list.value.length)
+ const spySrcFn = vi.fn(() => list.value)
+
+ const vDirective: Directive = {
+ created: (el, { value }) => calls.push(`${value} created`),
+ beforeMount: (el, { value }) => calls.push(`${value} beforeMount`),
+ mounted: (el, { value }) => calls.push(`${value} mounted`),
+ beforeUpdate: (el, { value }) => calls.push(`${value} beforeUpdate`),
+ updated: (el, { value }) => calls.push(`${value} updated`),
+ beforeUnmount: (el, { value }) => calls.push(`${value} beforeUnmount`),
+ unmounted: (el, { value }) => calls.push(`${value} unmounted`),
+ }
+
+ const t0 = template('')
+ const { instance } = define(() => {
+ const n1 = createFor(spySrcFn, block => {
+ const n2 = t0()
+ const n3 = children(n2, 0)
+ withDirectives(n3, [[vDirective, () => block.s[0]]])
+ return [n2, NOOP]
+ })
+ renderEffect(() => update.value)
+ return [n1]
+ }).render()
+
+ await nextTick()
+ // `${item index} ${hook name}`
+ expect(calls).toEqual(['0 created', '0 beforeMount', '0 mounted'])
+ calls.length = 0
+ expect(spySrcFn).toHaveBeenCalledTimes(1)
+
+ add()
+ await nextTick()
+ expect(calls).toEqual([
+ '0 beforeUpdate',
+ '1 created',
+ '1 beforeMount',
+ '0 updated',
+ '1 mounted',
+ ])
+ calls.length = 0
+ expect(spySrcFn).toHaveBeenCalledTimes(2)
+
+ list.value.reverse()
+ await nextTick()
+ expect(calls).toEqual([
+ '1 beforeUpdate',
+ '0 beforeUpdate',
+ '1 updated',
+ '0 updated',
+ ])
+ expect(spySrcFn).toHaveBeenCalledTimes(3)
+ list.value.reverse()
+ await nextTick()
+ calls.length = 0
+ expect(spySrcFn).toHaveBeenCalledTimes(4)
+
+ update.value++
+ await nextTick()
+ expect(calls).toEqual([
+ '0 beforeUpdate',
+ '1 beforeUpdate',
+ '0 updated',
+ '1 updated',
+ ])
+ calls.length = 0
+ expect(spySrcFn).toHaveBeenCalledTimes(4)
+
+ list.value.pop()
+ await nextTick()
+ expect(calls).toEqual([
+ '0 beforeUpdate',
+ '1 beforeUnmount',
+ '0 updated',
+ '1 unmounted',
+ ])
+ calls.length = 0
+ expect(spySrcFn).toHaveBeenCalledTimes(5)
+
+ unmountComponent(instance)
+ expect(calls).toEqual(['0 beforeUnmount', '0 unmounted'])
+ expect(spySrcFn).toHaveBeenCalledTimes(5)
+ })
})
diff --git a/packages/runtime-vapor/__tests__/if.spec.ts b/packages/runtime-vapor/__tests__/if.spec.ts
index ad03753b0..03a94ff36 100644
--- a/packages/runtime-vapor/__tests__/if.spec.ts
+++ b/packages/runtime-vapor/__tests__/if.spec.ts
@@ -1,4 +1,5 @@
import {
+ children,
createIf,
insert,
nextTick,
@@ -6,9 +7,11 @@ import {
renderEffect,
setText,
template,
+ withDirectives,
} from '../src'
import type { Mock } from 'vitest'
import { makeRender } from './_utils'
+import { unmountComponent } from '../src/apiRender'
const define = makeRender()
@@ -24,6 +27,8 @@ describe('createIf', () => {
let spyElseFn: Mock
const count = ref(0)
+ const spyConditionFn = vi.fn(() => count.value)
+
// templates can be reused through caching.
const t0 = template('')
const t1 = template('')
@@ -34,7 +39,7 @@ describe('createIf', () => {
insert(
createIf(
- () => count.value,
+ spyConditionFn,
// v-if
(spyIfFn ||= vi.fn(() => {
const n2 = t1()
@@ -55,24 +60,28 @@ describe('createIf', () => {
}).render()
expect(host.innerHTML).toBe('')
+ expect(spyConditionFn).toHaveBeenCalledTimes(1)
expect(spyIfFn!).toHaveBeenCalledTimes(0)
expect(spyElseFn!).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(host.innerHTML).toBe('')
+ expect(spyConditionFn).toHaveBeenCalledTimes(2)
expect(spyIfFn!).toHaveBeenCalledTimes(1)
expect(spyElseFn!).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(host.innerHTML).toBe('')
+ expect(spyConditionFn).toHaveBeenCalledTimes(3)
expect(spyIfFn!).toHaveBeenCalledTimes(1)
expect(spyElseFn!).toHaveBeenCalledTimes(1)
count.value = 0
await nextTick()
expect(host.innerHTML).toBe('')
+ expect(spyConditionFn).toHaveBeenCalledTimes(4)
expect(spyIfFn!).toHaveBeenCalledTimes(1)
expect(spyElseFn!).toHaveBeenCalledTimes(2)
})
@@ -124,4 +133,113 @@ describe('createIf', () => {
await nextTick()
expect(host.innerHTML).toBe('')
})
+
+ test('should work with directive hooks', async () => {
+ const calls: string[] = []
+ const show1 = ref(true)
+ const show2 = ref(true)
+ const update = ref(0)
+
+ const spyConditionFn1 = vi.fn(() => show1.value)
+ const spyConditionFn2 = vi.fn(() => show2.value)
+
+ const vDirective: any = {
+ created: (el: any, { value }: any) => calls.push(`${value} created`),
+ beforeMount: (el: any, { value }: any) =>
+ calls.push(`${value} beforeMount`),
+ mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
+ beforeUpdate: (el: any, { value }: any) =>
+ calls.push(`${value} beforeUpdate`),
+ updated: (el: any, { value }: any) => calls.push(`${value} updated`),
+ beforeUnmount: (el: any, { value }: any) =>
+ calls.push(`${value} beforeUnmount`),
+ unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
+ }
+
+ const t0 = template('')
+ const { instance } = define(() => {
+ const n1 = createIf(
+ spyConditionFn1,
+ () => {
+ const n2 = t0()
+ withDirectives(children(n2, 0), [
+ [vDirective, () => (update.value, '1')],
+ ])
+ return n2
+ },
+ () =>
+ createIf(
+ spyConditionFn2,
+ () => {
+ const n2 = t0()
+ withDirectives(children(n2, 0), [[vDirective, () => '2']])
+ return n2
+ },
+ () => {
+ const n2 = t0()
+ withDirectives(children(n2, 0), [[vDirective, () => '3']])
+ return n2
+ },
+ ),
+ )
+ return [n1]
+ }).render()
+
+ await nextTick()
+ expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
+ calls.length = 0
+ expect(spyConditionFn1).toHaveBeenCalledTimes(1)
+ expect(spyConditionFn2).toHaveBeenCalledTimes(0)
+
+ show1.value = false
+ await nextTick()
+ expect(calls).toEqual([
+ '1 beforeUnmount',
+ '2 created',
+ '2 beforeMount',
+ '1 unmounted',
+ '2 mounted',
+ ])
+ calls.length = 0
+ expect(spyConditionFn1).toHaveBeenCalledTimes(2)
+ expect(spyConditionFn2).toHaveBeenCalledTimes(1)
+
+ show2.value = false
+ await nextTick()
+ expect(calls).toEqual([
+ '2 beforeUnmount',
+ '3 created',
+ '3 beforeMount',
+ '2 unmounted',
+ '3 mounted',
+ ])
+ calls.length = 0
+ expect(spyConditionFn1).toHaveBeenCalledTimes(2)
+ expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+
+ show1.value = true
+ await nextTick()
+ expect(calls).toEqual([
+ '3 beforeUnmount',
+ '1 created',
+ '1 beforeMount',
+ '3 unmounted',
+ '1 mounted',
+ ])
+ calls.length = 0
+ expect(spyConditionFn1).toHaveBeenCalledTimes(3)
+ expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+
+ update.value++
+ await nextTick()
+ expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
+ calls.length = 0
+ expect(spyConditionFn1).toHaveBeenCalledTimes(3)
+ expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+
+ unmountComponent(instance)
+ expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
+ expect(spyConditionFn1).toHaveBeenCalledTimes(3)
+ expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+ })
})
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index fd7274c56..3bad7f36b 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -1,14 +1,26 @@
-import { type EffectScope, effectScope, isReactive } from '@vue/reactivity'
+import { getCurrentScope, isReactive, traverse } from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared'
-import { createComment, createTextNode, insert, remove } from './dom/element'
-import { renderEffect } from './renderEffect'
+import {
+ createComment,
+ createTextNode,
+ insert,
+ remove as removeBlock,
+} from './dom/element'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { warn } from './warning'
+import { currentInstance } from './component'
import { componentKey } from './component'
+import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
+import {
+ createChildFragmentDirectives,
+ invokeWithMount,
+ invokeWithUnmount,
+ invokeWithUpdate,
+} from './directivesChildFragment'
import type { DynamicSlot } from './componentSlots'
interface ForBlock extends Fragment {
- scope: EffectScope
+ scope: BlockEffectScope
/** state, use short key since it's used a lot in generated code */
s: [item: any, key: any, index?: number]
update: () => void
@@ -16,9 +28,11 @@ interface ForBlock extends Fragment {
memo: any[] | undefined
}
+type Source = any[] | Record | number | Set | Map
+
/*! #__NO_SIDE_EFFECTS__ */
export const createFor = (
- src: () => any[] | Record | number | Set | Map,
+ src: () => Source,
renderItem: (block: ForBlock) => [Block, () => void],
getKey?: (item: any, key: any, index?: number) => any,
getMemo?: (item: any, key: any, index?: number) => any[],
@@ -29,18 +43,34 @@ export const createFor = (
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
+ const update = getMemo ? updateWithMemo : updateWithoutMemo
+ const parentScope = getCurrentScope()!
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
const ref: Fragment = {
nodes: oldBlocks,
[fragmentKey]: true,
}
- const update = getMemo ? updateWithMemo : updateWithoutMemo
- once ? renderList() : renderEffect(renderList)
+
+ const instance = currentInstance!
+ if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
+ warn('createFor() can only be used inside setup()')
+ }
+
+ createChildFragmentDirectives(
+ parentAnchor,
+ () => oldBlocks.map(b => b.scope),
+ // source getter
+ () => traverse(src(), 1),
+ // init cb
+ getValue => doFor(getValue()),
+ // effect cb
+ getValue => doFor(getValue()),
+ once,
+ )
return ref
- function renderList() {
- const source = src()
+ function doFor(source: any) {
const newLength = getLength(source)
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)
@@ -225,7 +255,8 @@ export const createFor = (
idx: number,
anchor: Node = parentAnchor,
): ForBlock {
- const scope = effectScope()
+ const scope = new BlockEffectScope(instance, parentScope)
+
const [item, key, index] = getItem(source, idx)
const block: ForBlock = (newBlocks[idx] = {
nodes: null!, // set later
@@ -239,8 +270,12 @@ export const createFor = (
const res = scope.run(() => renderItem(block))!
block.nodes = res[0]
block.update = res[1]
- if (getMemo) block.update()
- if (parent) insert(block.nodes, parent, anchor)
+
+ invokeWithMount(scope, () => {
+ if (getMemo) block.update()
+ if (parent) insert(block.nodes, parent, anchor)
+ })
+
return block
}
@@ -275,10 +310,13 @@ export const createFor = (
}
}
}
- if (needsUpdate) {
- block.s = [newItem, newKey, newIndex]
- block.update()
- }
+
+ block.s = [newItem, newKey, newIndex]
+ invokeWithUpdate(block.scope, () => {
+ if (needsUpdate) {
+ block.update()
+ }
+ })
}
function updateWithoutMemo(
@@ -287,20 +325,24 @@ export const createFor = (
newKey = block.s[1],
newIndex = block.s[2],
) {
- if (
+ let needsUpdate =
newItem !== block.s[0] ||
newKey !== block.s[1] ||
newIndex !== block.s[2] ||
!isReactive(newItem)
- ) {
- block.s = [newItem, newKey, newIndex]
- block.update()
- }
+
+ block.s = [newItem, newKey, newIndex]
+ invokeWithUpdate(block.scope, () => {
+ if (needsUpdate) {
+ block.update()
+ }
+ })
}
function unmount({ nodes, scope }: ForBlock) {
- remove(nodes, parent!)
- scope.stop()
+ invokeWithUnmount(scope, () => {
+ removeBlock(nodes, parent!)
+ })
}
}
diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts
index b05fc3b05..dce38bae9 100644
--- a/packages/runtime-vapor/src/apiCreateIf.ts
+++ b/packages/runtime-vapor/src/apiCreateIf.ts
@@ -1,7 +1,15 @@
-import { renderEffect } from './renderEffect'
import { type Block, type Fragment, fragmentKey } from './apiRender'
-import { type EffectScope, effectScope } from '@vue/reactivity'
+import { getCurrentScope } from '@vue/reactivity'
import { createComment, createTextNode, insert, remove } from './dom/element'
+import { currentInstance } from './component'
+import { warn } from './warning'
+import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
+import {
+ createChildFragmentDirectives,
+ invokeWithMount,
+ invokeWithUnmount,
+ invokeWithUpdate,
+} from './directivesChildFragment'
type BlockFn = () => Block
@@ -18,7 +26,8 @@ export const createIf = (
let branch: BlockFn | undefined
let parent: ParentNode | undefined | null
let block: Block | undefined
- let scope: EffectScope | undefined
+ let scope: BlockEffectScope | undefined
+ const parentScope = getCurrentScope()!
const anchor = __DEV__ ? createComment('if') : createTextNode()
const fragment: Fragment = {
nodes: [],
@@ -26,35 +35,37 @@ export const createIf = (
[fragmentKey]: true,
}
+ const instance = currentInstance!
+ if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
+ warn('createIf() can only be used inside setup()')
+ }
+
// TODO: SSR
// if (isHydrating) {
// parent = hydrationNode!.parentNode
// setCurrentHydrationNode(hydrationNode!)
// }
- if (once) {
- doIf()
- } else {
- renderEffect(() => doIf())
- }
-
- function doIf() {
- if ((newValue = !!condition()) !== oldValue) {
- parent ||= anchor.parentNode
- if (block) {
- scope!.stop()
- remove(block, parent!)
- }
- if ((branch = (oldValue = newValue) ? b1 : b2)) {
- scope = effectScope()
- fragment.nodes = block = scope.run(branch)!
- parent && insert(block, parent, anchor)
- } else {
- scope = block = undefined
- fragment.nodes = []
+ createChildFragmentDirectives(
+ anchor,
+ () => (scope ? [scope] : []),
+ // source getter
+ condition,
+ // init cb
+ getValue => {
+ newValue = !!getValue()
+ doIf()
+ },
+ // effect cb
+ getValue => {
+ if ((newValue = !!getValue()) !== oldValue) {
+ doIf()
+ } else if (scope) {
+ invokeWithUpdate(scope)
}
- }
- }
+ },
+ once,
+ )
// TODO: SSR
// if (isHydrating) {
@@ -62,4 +73,19 @@ export const createIf = (
// }
return fragment
+
+ function doIf() {
+ parent ||= anchor.parentNode
+ if (block) {
+ invokeWithUnmount(scope!, () => remove(block!, parent!))
+ }
+ if ((branch = (oldValue = newValue) ? b1 : b2)) {
+ scope = new BlockEffectScope(instance, parentScope)
+ fragment.nodes = block = scope.run(branch)!
+ invokeWithMount(scope, () => parent && insert(block!, parent, anchor))
+ } else {
+ scope = block = undefined
+ fragment.nodes = []
+ }
+ }
}
diff --git a/packages/runtime-vapor/src/blockEffectScope.ts b/packages/runtime-vapor/src/blockEffectScope.ts
new file mode 100644
index 000000000..92fc01983
--- /dev/null
+++ b/packages/runtime-vapor/src/blockEffectScope.ts
@@ -0,0 +1,36 @@
+import { EffectScope } from '@vue/reactivity'
+import type { ComponentInternalInstance } from './component'
+import type { DirectiveBindingsMap } from './directives'
+
+export class BlockEffectScope extends EffectScope {
+ /**
+ * instance
+ * @internal
+ */
+ it: ComponentInternalInstance
+ /**
+ * isMounted
+ * @internal
+ */
+ im: boolean
+ /**
+ * directives
+ * @internal
+ */
+ dirs?: DirectiveBindingsMap
+
+ constructor(
+ instance: ComponentInternalInstance,
+ parentScope: EffectScope | null,
+ ) {
+ super(false, parentScope || undefined)
+ this.im = false
+ this.it = instance
+ }
+}
+
+export function isRenderEffectScope(
+ scope: EffectScope | undefined,
+): scope is BlockEffectScope {
+ return scope instanceof BlockEffectScope
+}
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index bb98adc85..28ca6c2ab 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -1,7 +1,6 @@
-import { EffectScope, isRef } from '@vue/reactivity'
+import { isRef } from '@vue/reactivity'
import { EMPTY_OBJ, hasOwn, isArray, isFunction } from '@vue/shared'
import type { Block } from './apiRender'
-import type { DirectiveBinding } from './directives'
import {
type ComponentPropsOptions,
type NormalizedPropsOptions,
@@ -27,6 +26,7 @@ import { VaporLifecycleHooks } from './apiLifecycle'
import { warn } from './warning'
import { type AppContext, createAppContext } from './apiCreateVaporApp'
import type { Data } from '@vue/runtime-shared'
+import { BlockEffectScope } from './blockEffectScope'
export type Component = FunctionalComponent | ObjectComponent
@@ -154,10 +154,9 @@ export interface ComponentInternalInstance {
parent: ComponentInternalInstance | null
provides: Data
- scope: EffectScope
+ scope: BlockEffectScope
component: Component
comps: Set
- dirs: Map
rawProps: NormalizedRawProps
propsOptions: NormalizedPropsOptions
@@ -280,11 +279,10 @@ export function createComponentInstance(
parent,
- scope: new EffectScope(true /* detached */)!,
+ scope: null!,
provides: parent ? parent.provides : Object.create(_appContext.provides),
component,
comps: new Set(),
- dirs: new Map(),
// resolved props and emits options
rawProps: null!, // set later
@@ -355,6 +353,7 @@ export function createComponentInstance(
*/
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
}
+ instance.scope = new BlockEffectScope(instance, parent && parent.scope)
initProps(instance, rawProps, !isFunction(component), once)
initSlots(instance, slots, dynamicSlots)
instance.emit = emit.bind(null, instance)
diff --git a/packages/runtime-vapor/src/componentLifecycle.ts b/packages/runtime-vapor/src/componentLifecycle.ts
index c67093e27..bb97b089c 100644
--- a/packages/runtime-vapor/src/componentLifecycle.ts
+++ b/packages/runtime-vapor/src/componentLifecycle.ts
@@ -25,7 +25,7 @@ export function invokeLifecycle(
post ? queuePostFlushCb(fn) : fn()
}
- invokeDirectiveHook(instance, directive)
+ invokeDirectiveHook(instance, directive, instance.scope)
}
function invokeSub() {
diff --git a/packages/runtime-vapor/src/directives.ts b/packages/runtime-vapor/src/directives.ts
index ab295678b..3ec374623 100644
--- a/packages/runtime-vapor/src/directives.ts
+++ b/packages/runtime-vapor/src/directives.ts
@@ -1,32 +1,48 @@
-import { isFunction } from '@vue/shared'
+import { invokeArrayFns, isFunction } from '@vue/shared'
import {
type ComponentInternalInstance,
currentInstance,
isVaporComponent,
+ setCurrentInstance,
} from './component'
-import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
-import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
-import { renderEffect } from './renderEffect'
+import {
+ EffectFlags,
+ ReactiveEffect,
+ type SchedulerJob,
+ getCurrentScope,
+ pauseTracking,
+ resetTracking,
+ traverse,
+} from '@vue/reactivity'
+import {
+ VaporErrorCodes,
+ callWithAsyncErrorHandling,
+ callWithErrorHandling,
+} from './errorHandling'
+import { queueJob, queuePostFlushCb } from './scheduler'
import { warn } from './warning'
+import { type BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
import { normalizeBlock } from './dom/element'
export type DirectiveModifiers = Record
-export interface DirectiveBinding {
+export interface DirectiveBinding {
instance: ComponentInternalInstance
source?: () => V
value: V
oldValue: V | null
arg?: string
modifiers?: DirectiveModifiers
- dir: ObjectDirective
+ dir: ObjectDirective
}
+export type DirectiveBindingsMap = Map
+
export type DirectiveHook<
T = any | null,
V = any,
M extends string = string,
-> = (node: T, binding: DirectiveBinding) => void
+> = (node: T, binding: DirectiveBinding) => void
// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
// effect update -> `beforeUpdate` -> node updated -> `updated`
@@ -43,7 +59,7 @@ export type ObjectDirective = {
[K in DirectiveHookName]?: DirectiveHook | undefined
} & {
/** Watch value deeply */
- deep?: boolean
+ deep?: boolean | number
}
export type FunctionDirective<
@@ -86,9 +102,18 @@ export function withDirectives(
node = nodeOrComponent
}
- const instance = currentInstance
- if (!instance.dirs.has(node)) instance.dirs.set(node, [])
- const bindings = instance.dirs.get(node)!
+ let bindings: DirectiveBinding[]
+ const instance = currentInstance!
+ const parentScope = getCurrentScope() as BlockEffectScope
+
+ if (__DEV__ && !isRenderEffectScope(parentScope)) {
+ warn(`Directives should be used inside of RenderEffectScope.`)
+ }
+
+ const directivesMap = (parentScope.dirs ||= new Map())
+ if (!(bindings = directivesMap.get(node))) {
+ directivesMap.set(node, (bindings = []))
+ }
for (const directive of directives) {
let [dir, source, arg, modifiers] = directive
@@ -103,25 +128,38 @@ export function withDirectives(
const binding: DirectiveBinding = {
dir,
instance,
- source,
value: null, // set later
oldValue: undefined,
arg,
modifiers,
}
- bindings.push(binding)
-
- callDirectiveHook(node, binding, instance, 'created')
- // register source
if (source) {
if (dir.deep) {
const deep = dir.deep === true ? undefined : dir.deep
const baseSource = source
source = () => traverse(baseSource(), deep)
}
- renderEffect(source)
+
+ const effect = new ReactiveEffect(() =>
+ callWithErrorHandling(
+ source!,
+ instance,
+ VaporErrorCodes.RENDER_FUNCTION,
+ ),
+ )
+ const triggerRenderingUpdate = createRenderingUpdateTrigger(
+ instance,
+ effect,
+ )
+ effect.scheduler = () => queueJob(triggerRenderingUpdate)
+
+ binding.source = effect.run.bind(effect)
}
+
+ bindings.push(binding)
+
+ callDirectiveHook(node, binding, instance, 'created')
}
return nodeOrComponent
@@ -145,13 +183,14 @@ function getComponentNode(component: ComponentInternalInstance) {
export function invokeDirectiveHook(
instance: ComponentInternalInstance | null,
name: DirectiveHookName,
- nodes?: IterableIterator,
+ scope: BlockEffectScope,
) {
- if (!instance) return
- nodes = nodes || instance.dirs.keys()
- for (const node of nodes) {
- const directives = instance.dirs.get(node) || []
- for (const binding of directives) {
+ const { dirs } = scope
+ if (name === 'mounted') scope.im = true
+ if (!dirs) return
+ const iterator = dirs.entries()
+ for (const [node, bindings] of iterator) {
+ for (const binding of bindings) {
callDirectiveHook(node, binding, instance, name)
}
}
@@ -179,3 +218,43 @@ function callDirectiveHook(
])
resetTracking()
}
+
+export function createRenderingUpdateTrigger(
+ instance: ComponentInternalInstance,
+ effect: ReactiveEffect,
+): SchedulerJob {
+ job.id = instance.uid
+ return job
+ function job() {
+ if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
+ return
+ }
+
+ if (instance.isMounted && !instance.isUpdating) {
+ instance.isUpdating = true
+ const reset = setCurrentInstance(instance)
+
+ const { bu, u, scope } = instance
+ const { dirs } = scope
+ // beforeUpdate hook
+ if (bu) {
+ invokeArrayFns(bu)
+ }
+ invokeDirectiveHook(instance, 'beforeUpdate', scope)
+
+ queuePostFlushCb(() => {
+ instance.isUpdating = false
+ const reset = setCurrentInstance(instance)
+ if (dirs) {
+ invokeDirectiveHook(instance, 'updated', scope)
+ }
+ // updated hook
+ if (u) {
+ queuePostFlushCb(u)
+ }
+ reset()
+ })
+ reset()
+ }
+ }
+}
diff --git a/packages/runtime-vapor/src/directivesChildFragment.ts b/packages/runtime-vapor/src/directivesChildFragment.ts
new file mode 100644
index 000000000..ff2f6e72f
--- /dev/null
+++ b/packages/runtime-vapor/src/directivesChildFragment.ts
@@ -0,0 +1,152 @@
+import { ReactiveEffect, getCurrentScope } from '@vue/reactivity'
+import {
+ type Directive,
+ type DirectiveHookName,
+ createRenderingUpdateTrigger,
+ invokeDirectiveHook,
+} from './directives'
+import { warn } from './warning'
+import { type BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
+import { currentInstance } from './component'
+import { VaporErrorCodes, callWithErrorHandling } from './errorHandling'
+import { queueJob, queuePostFlushCb } from './scheduler'
+
+/**
+ * used in createIf and createFor
+ * manage directives of child fragments in components.
+ */
+export function createChildFragmentDirectives(
+ anchor: Node,
+ getScopes: () => BlockEffectScope[],
+ source: () => any,
+ initCallback: (getValue: () => any) => void,
+ effectCallback: (getValue: () => any) => void,
+ once?: boolean,
+) {
+ let isTriggered = false
+ const instance = currentInstance!
+ const parentScope = getCurrentScope() as BlockEffectScope
+ if (__DEV__) {
+ if (!isRenderEffectScope(parentScope)) {
+ warn('child directives can only be added to a render effect scope')
+ }
+ if (!instance) {
+ warn('child directives can only be added in a component')
+ }
+ }
+
+ const callSourceWithErrorHandling = () =>
+ callWithErrorHandling(source, instance, VaporErrorCodes.RENDER_FUNCTION)
+
+ if (once) {
+ initCallback(callSourceWithErrorHandling)
+ return
+ }
+
+ const directiveBindingsMap = (parentScope.dirs ||= new Map())
+ const dir: Directive = {
+ beforeUpdate: onDirectiveBeforeUpdate,
+ beforeMount: () => invokeChildrenDirectives('beforeMount'),
+ mounted: () => invokeChildrenDirectives('mounted'),
+ beforeUnmount: () => invokeChildrenDirectives('beforeUnmount'),
+ unmounted: () => invokeChildrenDirectives('unmounted'),
+ }
+ directiveBindingsMap.set(anchor, [
+ {
+ dir,
+ instance,
+ value: null,
+ oldValue: undefined,
+ },
+ ])
+
+ const effect = new ReactiveEffect(callSourceWithErrorHandling)
+ const triggerRenderingUpdate = createRenderingUpdateTrigger(instance, effect)
+ effect.scheduler = () => {
+ isTriggered = true
+ queueJob(triggerRenderingUpdate)
+ }
+
+ const getValue = () => effect.run()
+
+ initCallback(getValue)
+
+ function onDirectiveBeforeUpdate() {
+ if (isTriggered) {
+ isTriggered = false
+ effectCallback(getValue)
+ } else {
+ const scopes = getScopes()
+ for (const scope of scopes) {
+ invokeWithUpdate(scope)
+ }
+ return
+ }
+ }
+
+ function invokeChildrenDirectives(name: DirectiveHookName) {
+ const scopes = getScopes()
+ for (const scope of scopes) {
+ invokeDirectiveHook(instance, name, scope)
+ }
+ }
+}
+
+export function invokeWithMount(scope: BlockEffectScope, handler?: () => any) {
+ if (isRenderEffectScope(scope.parent) && !scope.parent.im) {
+ return handler && handler()
+ }
+ return invokeWithDirsHooks(scope, 'mount', handler)
+}
+
+export function invokeWithUnmount(
+ scope: BlockEffectScope,
+ handler?: () => void,
+) {
+ try {
+ return invokeWithDirsHooks(scope, 'unmount', handler)
+ } finally {
+ scope.stop()
+ }
+}
+
+export function invokeWithUpdate(
+ scope: BlockEffectScope,
+ handler?: () => void,
+) {
+ return invokeWithDirsHooks(scope, 'update', handler)
+}
+
+const lifecycleMap = {
+ mount: ['beforeMount', 'mounted'],
+ update: ['beforeUpdate', 'updated'],
+ unmount: ['beforeUnmount', 'unmounted'],
+} as const
+
+function invokeWithDirsHooks(
+ scope: BlockEffectScope,
+ name: keyof typeof lifecycleMap,
+ handler?: () => any,
+) {
+ const { dirs, it: instance } = scope
+ const [before, after] = lifecycleMap[name]
+
+ if (!dirs) {
+ const res = handler && handler()
+ if (name === 'mount') {
+ queuePostFlushCb(() => (scope.im = true))
+ }
+ return res
+ }
+
+ invokeDirectiveHook(instance, before, scope)
+ try {
+ if (handler) {
+ return handler()
+ }
+ } finally {
+ queuePostFlushCb(() => {
+ invokeDirectiveHook(instance, after, scope)
+ })
+ }
+}
diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts
index bdb418897..dcd7088cd 100644
--- a/packages/runtime-vapor/src/renderEffect.ts
+++ b/packages/runtime-vapor/src/renderEffect.ts
@@ -18,73 +18,77 @@ import { invokeDirectiveHook } from './directives'
export function renderEffect(cb: () => void) {
const instance = getCurrentInstance()
const scope = getCurrentScope()
- let effect: ReactiveEffect
- const job: SchedulerJob = () => {
+ if (scope) {
+ const baseCb = cb
+ cb = () => scope.run(baseCb)
+ }
+
+ if (instance) {
+ const baseCb = cb
+ cb = () => {
+ const reset = setCurrentInstance(instance)
+ baseCb()
+ reset()
+ }
+ job.id = instance.uid
+ }
+
+ const effect = new ReactiveEffect(() =>
+ callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
+ )
+
+ effect.scheduler = () => queueJob(job)
+ if (__DEV__ && instance) {
+ effect.onTrack = instance.rtc
+ ? e => invokeArrayFns(instance.rtc!, e)
+ : void 0
+ effect.onTrigger = instance.rtg
+ ? e => invokeArrayFns(instance.rtg!, e)
+ : void 0
+ }
+ effect.run()
+
+ function job() {
if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
return
}
+ const reset = instance && setCurrentInstance(instance)
+
if (instance && instance.isMounted && !instance.isUpdating) {
instance.isUpdating = true
- const { bu, u, dirs } = instance
+ const { bu, u, scope } = instance
+ const { dirs } = scope
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
if (dirs) {
- invokeDirectiveHook(instance, 'beforeUpdate')
+ invokeDirectiveHook(instance, 'beforeUpdate', scope)
}
effect.run()
queuePostFlushCb(() => {
instance.isUpdating = false
+ const reset = setCurrentInstance(instance)
if (dirs) {
- invokeDirectiveHook(instance, 'updated')
+ invokeDirectiveHook(instance, 'updated', scope)
}
// updated hook
if (u) {
queuePostFlushCb(u)
}
+ reset()
})
} else {
effect.run()
}
- }
-
- if (scope) {
- const baseCb = cb
- cb = () => scope.run(baseCb)
- }
-
- if (instance) {
- const baseCb = cb
- cb = () => {
- const reset = setCurrentInstance(instance)
- baseCb()
- reset()
- }
- }
- effect = new ReactiveEffect(() =>
- callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
- )
-
- effect.scheduler = () => {
- if (instance) job.id = instance.uid
- queueJob(job)
- }
- if (__DEV__ && instance) {
- effect.onTrack = instance.rtc
- ? e => invokeArrayFns(instance.rtc!, e)
- : void 0
- effect.onTrigger = instance.rtg
- ? e => invokeArrayFns(instance.rtg!, e)
- : void 0
+ reset && reset()
}
- effect.run()
}
export function firstEffect(