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('

zero

') + expect(spyIfFn!).toHaveBeenCalledTimes(0) + expect(spyElseFn!).toHaveBeenCalledTimes(1) + + add() + await nextTick() + expect(host.innerHTML).toBe('

1

') + expect(spyIfFn!).toHaveBeenCalledTimes(1) + expect(spyElseFn!).toHaveBeenCalledTimes(1) + + add() + await nextTick() + expect(host.innerHTML).toBe('

2

') + expect(spyIfFn!).toHaveBeenCalledTimes(1) + expect(spyElseFn!).toHaveBeenCalledTimes(1) + + reset() + await nextTick() + expect(host.innerHTML).toBe('

zero

') + 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