diff --git a/packages/runtime-vapor/__tests__/if.spec.ts b/packages/runtime-vapor/__tests__/if.spec.ts
new file mode 100644
index 000000000..790e36cc4
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/if.spec.ts
@@ -0,0 +1,112 @@
+import { defineComponent } from 'vue'
+import {
+ children,
+ createIf,
+ insert,
+ nextTick,
+ ref,
+ render,
+ renderEffect,
+ setText,
+ template,
+} from '../src'
+import { NOOP } from '@vue/shared'
+import type { Mock } from 'vitest'
+
+let host: HTMLElement
+
+const initHost = () => {
+ host = document.createElement('div')
+ host.setAttribute('id', 'host')
+ document.body.appendChild(host)
+}
+beforeEach(() => {
+ initHost()
+})
+afterEach(() => {
+ host.remove()
+})
+
+describe('createIf', () => {
+ test('basic', async () => {
+ // mock this template:
+ //
+ //
{{counter}}
+ //
zero
+ //
+
+ let spyIfFn: Mock
+ let spyElseFn: Mock
+
+ let add = NOOP
+ let reset = NOOP
+
+ // templates can be reused through caching.
+ const t0 = template('')
+ const t1 = template('')
+ const t2 = template('zero
')
+
+ const component = defineComponent({
+ setup() {
+ const counter = ref(0)
+ add = () => counter.value++
+ reset = () => (counter.value = 0)
+
+ // render
+ return (() => {
+ const n0 = t0()
+ const {
+ 0: [n1],
+ } = children(n0)
+
+ insert(
+ createIf(
+ () => counter.value,
+ // v-if
+ (spyIfFn ||= vi.fn(() => {
+ const n2 = t1()
+ const {
+ 0: [n3],
+ } = children(n2)
+ renderEffect(() => {
+ setText(n3, void 0, counter.value)
+ })
+ return n2
+ })),
+ // v-else
+ (spyElseFn ||= vi.fn(() => {
+ const n4 = t2()
+ return n4
+ })),
+ ),
+ n1,
+ )
+ return n0
+ })()
+ },
+ })
+ render(component as any, {}, '#host')
+
+ expect(host.innerHTML).toBe('')
+ expect(spyIfFn!).toHaveBeenCalledTimes(0)
+ expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+ add()
+ await nextTick()
+ expect(host.innerHTML).toBe('')
+ expect(spyIfFn!).toHaveBeenCalledTimes(1)
+ expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+ add()
+ await nextTick()
+ expect(host.innerHTML).toBe('')
+ expect(spyIfFn!).toHaveBeenCalledTimes(1)
+ expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+ reset()
+ await nextTick()
+ expect(host.innerHTML).toBe('')
+ expect(spyIfFn!).toHaveBeenCalledTimes(1)
+ expect(spyElseFn!).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts
index 5c270d1ca..831a94b38 100644
--- a/packages/runtime-vapor/src/apiLifecycle.ts
+++ b/packages/runtime-vapor/src/apiLifecycle.ts
@@ -2,7 +2,6 @@ import {
type ComponentInternalInstance,
currentInstance,
setCurrentInstance,
- unsetCurrentInstance,
} from './component'
import { warn } from './warning'
import { pauseTracking, resetTracking } from '@vue/reactivity'
@@ -25,9 +24,9 @@ export const injectHook = (
return
}
pauseTracking()
- setCurrentInstance(target)
+ const reset = setCurrentInstance(target)
const res = callWithAsyncErrorHandling(hook, target, type, args)
- unsetCurrentInstance()
+ reset()
resetTracking()
return res
})
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index ab2e49c3a..51be7f4ba 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -122,10 +122,17 @@ export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
+ const prev = currentInstance
currentInstance = instance
+ instance.scope.on()
+ return () => {
+ instance.scope.off()
+ currentInstance = prev
+ }
}
export const unsetCurrentInstance = () => {
+ currentInstance?.scope.off()
currentInstance = null
}
diff --git a/packages/runtime-vapor/src/dom.ts b/packages/runtime-vapor/src/dom.ts
index 47d888fae..1dda46f21 100644
--- a/packages/runtime-vapor/src/dom.ts
+++ b/packages/runtime-vapor/src/dom.ts
@@ -6,11 +6,7 @@ import {
} from '@vue/shared'
import type { Block, ParentBlock } from './render'
-export function insert(
- block: Block,
- parent: ParentNode,
- anchor: Node | null = null,
-) {
+export function insert(block: Block, parent: Node, anchor: Node | null = null) {
// if (!isHydrating) {
if (block instanceof Node) {
parent.insertBefore(block, anchor)
diff --git a/packages/runtime-vapor/src/if.ts b/packages/runtime-vapor/src/if.ts
new file mode 100644
index 000000000..4892e95ff
--- /dev/null
+++ b/packages/runtime-vapor/src/if.ts
@@ -0,0 +1,61 @@
+import { renderWatch } from './renderWatch'
+import type { BlockFn, Fragment } from './render'
+import { effectScope, onEffectCleanup } from '@vue/reactivity'
+import { insert, remove } from './dom'
+
+export const createIf = (
+ condition: () => any,
+ b1: BlockFn,
+ // 如果是 v-else-if 就把 () => createIf 作为 b2 传入
+ b2?: BlockFn,
+ hydrationNode?: Node,
+): Fragment => {
+ let branch: BlockFn | undefined
+ let parent: ParentNode | undefined | null
+ const anchor = __DEV__
+ ? // eslint-disable-next-line no-restricted-globals
+ document.createComment('if')
+ : // eslint-disable-next-line no-restricted-globals
+ document.createTextNode('')
+ const fragment: Fragment = { nodes: [], anchor }
+
+ // TODO: SSR
+ // if (isHydrating) {
+ // parent = hydrationNode!.parentNode
+ // setCurrentHydrationNode(hydrationNode!)
+ // }
+
+ renderWatch(
+ () => Boolean(condition()),
+ (value) => {
+ parent ||= anchor.parentNode
+ if ((branch = value ? b1 : b2)) {
+ let scope = effectScope()
+ let block = scope.run(branch)!
+
+ if (block instanceof DocumentFragment) {
+ block = Array.from(block.childNodes)
+ }
+ fragment.nodes = block
+
+ parent && insert(block, parent, anchor)
+
+ onEffectCleanup(() => {
+ parent ||= anchor.parentNode
+ scope.stop()
+ remove(block, parent!)
+ })
+ } else {
+ fragment.nodes = []
+ }
+ },
+ { immediate: true },
+ )
+
+ // TODO: SSR
+ // if (isHydrating) {
+ // parent!.insertBefore(anchor, currentHydrationNode)
+ // }
+
+ return fragment
+}
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
index 805fbae0e..0fc7be64a 100644
--- a/packages/runtime-vapor/src/index.ts
+++ b/packages/runtime-vapor/src/index.ts
@@ -50,3 +50,4 @@ export * from './dom'
export * from './directives/vShow'
export * from './apiLifecycle'
export { getCurrentInstance, type ComponentInternalInstance } from './component'
+export * from './if'
diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts
index bce3a2be7..56cd8ab08 100644
--- a/packages/runtime-vapor/src/render.ts
+++ b/packages/runtime-vapor/src/render.ts
@@ -14,7 +14,7 @@ import { insert, remove } from './dom'
export type Block = Node | Fragment | Block[]
export type ParentBlock = ParentNode | Node[]
export type Fragment = { nodes: Block; anchor: Node }
-export type BlockFn = (props: any, ctx: any) => Block
+export type BlockFn = (props?: any) => Block
let isRenderingActivity = false
export function getIsRendering() {
@@ -44,7 +44,7 @@ export function mountComponent(
) {
instance.container = container
- setCurrentInstance(instance)
+ const reset = setCurrentInstance(instance)
const block = instance.scope.run(() => {
const { component, props } = instance
const ctx = { expose: () => {} }
@@ -82,7 +82,7 @@ export function mountComponent(
// hook: mounted
invokeDirectiveHook(instance, 'mounted')
m && invokeArrayFns(m)
- unsetCurrentInstance()
+ reset
return instance
}
diff --git a/packages/runtime-vapor/src/renderWatch.ts b/packages/runtime-vapor/src/renderWatch.ts
index e5103d716..c0167cdf5 100644
--- a/packages/runtime-vapor/src/renderWatch.ts
+++ b/packages/runtime-vapor/src/renderWatch.ts
@@ -3,10 +3,13 @@ import {
type BaseWatchMiddleware,
type BaseWatchOptions,
baseWatch,
- getCurrentScope,
} from '@vue/reactivity'
-import { NOOP, invokeArrayFns, remove } from '@vue/shared'
-import { type ComponentInternalInstance, currentInstance } from './component'
+import { NOOP, extend, invokeArrayFns, remove } from '@vue/shared'
+import {
+ type ComponentInternalInstance,
+ getCurrentInstance,
+ setCurrentInstance,
+} from './component'
import {
createVaporRenderingScheduler,
queuePostRenderEffect,
@@ -15,6 +18,12 @@ import { handleError as handleErrorWithInstance } from './errorHandling'
import { warn } from './warning'
import { invokeDirectiveHook } from './directive'
+interface RenderWatchOptions {
+ immediate?: boolean
+ deep?: boolean
+ once?: boolean
+}
+
type WatchStopHandle = () => void
export function renderEffect(effect: () => void): WatchStopHandle {
@@ -24,20 +33,25 @@ export function renderEffect(effect: () => void): WatchStopHandle {
export function renderWatch(
source: any,
cb: (value: any, oldValue: any) => void,
+ options?: RenderWatchOptions,
): WatchStopHandle {
- return doWatch(source as any, cb)
+ return doWatch(source as any, cb, options)
}
-function doWatch(source: any, cb?: any): WatchStopHandle {
- const extendOptions: BaseWatchOptions = {}
+function doWatch(
+ source: any,
+ cb?: any,
+ options?: RenderWatchOptions,
+): WatchStopHandle {
+ const extendOptions: BaseWatchOptions =
+ cb && options ? extend({}, options) : {}
if (__DEV__) extendOptions.onWarn = warn
// TODO: SSR
// if (__SSR__) {}
- const instance =
- getCurrentScope() === currentInstance?.scope ? currentInstance : null
+ const instance = getCurrentInstance()
extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) =>
handleErrorWithInstance(err, instance, type)
@@ -78,8 +92,10 @@ const createMiddleware =
instance.isUpdating = true
}
+ const reset = setCurrentInstance(instance)
// run callback
value = next()
+ reset()
if (isFirstEffect) {
queuePostRenderEffect(() => {
diff --git a/packages/runtime-vapor/src/template.ts b/packages/runtime-vapor/src/template.ts
index f75ab8699..8b505a6ec 100644
--- a/packages/runtime-vapor/src/template.ts
+++ b/packages/runtime-vapor/src/template.ts
@@ -10,7 +10,7 @@ export const template = (str: string): (() => DocumentFragment) => {
// first render: insert the node directly.
// this removes it from the template fragment to avoid keeping two copies
// of the inserted tree in memory, even if the template is used only once.
- return (node = t.content)
+ return (node = t.content).cloneNode(true) as DocumentFragment
} else {
// repeated renders: clone from cache. This is more performant and
// efficient when dealing with big lists where the template is repeated