From: daiwei Date: Tue, 29 Jul 2025 05:59:41 +0000 (+0800) Subject: chore: Merge branch 'minor' into edison/testVapor X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=32b0bcb11c70ddb8aa3aa9e808d392da2a9b74bf;p=thirdparty%2Fvuejs%2Fcore.git chore: Merge branch 'minor' into edison/testVapor --- 32b0bcb11c70ddb8aa3aa9e808d392da2a9b74bf diff --cc packages/compiler-ssr/__tests__/ssrVModel.spec.ts index c88f6ba318,8a439dbf4b..f7b7a3241b --- a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts @@@ -167,6 -166,132 +167,135 @@@ describe('ssr: v-model', () => _push(\`\`) }" `) + + expect( + compileWithWrapper(` + `).code, + ).toMatchInlineSnapshot(` + "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) ++ _push(\`\`) + }" + `) + + expect( + compileWithWrapper(` + `).code, + ).toMatchInlineSnapshot(` + "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper(` + `).code, + ).toMatchInlineSnapshot(` + "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper(` + `).code, + ).toMatchInlineSnapshot(` + "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) ++ _push(\`\`) + }" + `) }) test('', () => { diff --cc packages/compiler-vapor/src/generators/block.ts index 0811921bd9,3034739475..d101962ba4 --- a/packages/compiler-vapor/src/generators/block.ts +++ b/packages/compiler-vapor/src/generators/block.ts @@@ -38,16 -36,12 +37,16 @@@ export function genBlockContent block: BlockIRNode, context: CodegenContext, root?: boolean, - customReturns?: (returns: CodeFragment[]) => CodeFragment[], + genEffectsExtraFrag?: () => CodeFragment[], ): CodeFragment[] { const [frag, push] = buildCodeFragment() - const { dynamic, effect, operation, returns } = block + const { dynamic, effect, operation, returns, key } = block const resetBlock = context.enterBlock(block) + if (block.hasDeferredVShow) { + push(NEWLINE, `const deferredApplyVShows = []`) + } + if (root) { for (let name of context.ir.component) { const id = toValidAssetId(name, 'component') @@@ -75,21 -69,8 +74,21 @@@ } push(...genOperations(operation, context)) - push(...genEffects(effect, context)) + push(...genEffects(effect, context, genEffectsExtraFrag)) + if (block.hasDeferredVShow) { + push(NEWLINE, `deferredApplyVShows.forEach(fn => fn())`) + } + + if (dynamic.needsKey) { + for (const child of dynamic.children) { + const keyValue = key + ? genExpression(key, context) + : JSON.stringify(child.id) + push(NEWLINE, `n${child.id}.$key = `, ...keyValue) + } + } + push(NEWLINE, `return `) const returnNodes = returns.map(n => `n${n}`) diff --cc packages/runtime-core/src/apiAsyncComponent.ts index 8eee833ac4,5c5c06c489..8841f669b7 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@@ -67,17 -123,30 +68,30 @@@ export function defineAsyncComponent __asyncLoader: load, __asyncHydrate(el, instance, hydrate) { + let patched = false const doHydrate = hydrateStrategy ? () => { - const teardown = hydrateStrategy(hydrate, cb => + const performHydrate = () => { + // skip hydration if the component has been patched + if (__DEV__ && patched) { + warn( - `Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` + ++ `Skipping lazy hydration for component '${getComponentName(getResolvedComp()!)}': ` + + `it was updated before lazy hydration performed.`, + ) + return + } + hydrate() + } + const teardown = hydrateStrategy(performHydrate, cb => forEachElement(el, cb), ) if (teardown) { ;(instance.bum || (instance.bum = [])).push(teardown) } + ;(instance.u || (instance.u = [])).push(() => (patched = true)) } : hydrate - if (resolvedComp) { + if (getResolvedComp()) { doHydrate() } else { load().then(() => !instance.isUnmounted && doHydrate()) diff --cc packages/runtime-core/src/apiCreateApp.ts index 2e70778fe9,a1409a7fe4..43c96e5003 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@@ -26,8 -26,8 +26,8 @@@ import type { InjectionKey } from './ap import { warn } from './warning' import type { VNode } from './vnode' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' - import { NO, extend, isFunction, isObject } from '@vue/shared' + import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared' -import { version } from '.' +import { type TransitionHooks, version } from '.' import { installAppCompatProperties } from './compat/global' import type { NormalizedPropsOptions } from './componentProps' import type { ObjectEmitsOptions } from './componentEmits' diff --cc packages/runtime-core/src/hydration.ts index 62bcb76867,15b3c7512b..5db9a3bdd5 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@@ -32,8 -31,8 +32,9 @@@ import isRenderableAttrValue, isReservedProp, isString, + isVaporAnchors, normalizeClass, + normalizeCssVarValue, normalizeStyle, stringifyStyle, } from '@vue/shared' diff --cc packages/runtime-core/src/renderer.ts index b968255aa5,30f9e6b2c1..d3c2972a7f --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@@ -732,22 -738,25 +739,26 @@@ function baseCreateRenderer } // #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved // #1689 For inside suspense + suspense resolved case, just call it - const needCallTransitionHooks = needTransition(parentSuspense, transition) - if (needCallTransitionHooks) { - transition!.beforeEnter(el) + if (transition) { + performTransitionEnter( + el, + transition, + () => hostInsert(el, container, anchor), + parentSuspense, + ) + } else { + hostInsert(el, container, anchor) } - hostInsert(el, container, anchor) - if ( - (vnodeHook = props && props.onVnodeMounted) || - needCallTransitionHooks || - dirs - ) { + + if ((vnodeHook = props && props.onVnodeMounted) || dirs) { - queuePostRenderEffect(() => { - vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) - dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') - }, parentSuspense) + queuePostRenderEffect( + () => { + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) - needCallTransitionHooks && transition!.enter(el) + dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') + }, + undefined, + parentSuspense, + ) } } @@@ -2638,50 -2744,7 +2713,50 @@@ export function invalidateMount(hooks: } } -function getVaporInterface( +// shared between vdom and vapor +export function performTransitionEnter( + el: RendererElement, + transition: TransitionHooks, + insert: () => void, + parentSuspense: SuspenseBoundary | null, +): void { + if (needTransition(parentSuspense, transition)) { + transition.beforeEnter(el) + insert() - queuePostRenderEffect(() => transition.enter(el), parentSuspense) ++ queuePostRenderEffect(() => transition.enter(el), undefined, parentSuspense) + } else { + insert() + } +} + +// shared between vdom and vapor +export function performTransitionLeave( + el: RendererElement, + transition: TransitionHooks, + remove: () => void, + isElement: boolean = true, +): void { + const performRemove = () => { + remove() + if (transition && !transition.persisted && transition.afterLeave) { + transition.afterLeave() + } + } + + if (isElement && transition && !transition.persisted) { + const { leave, delayLeave } = transition + const performLeave = () => leave(el, performRemove) + if (delayLeave) { + delayLeave(el, performRemove, performLeave) + } else { + performLeave() + } + } else { + performRemove() + } +} + +export function getVaporInterface( instance: ComponentInternalInstance | null, vnode: VNode, ): VaporInteropInterface { diff --cc packages/runtime-vapor/src/apiCreateFor.ts index f609543afc,dbc9ebc831..5bde0790c8 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@@ -10,16 -9,16 +9,11 @@@ import shallowRef, toReactive, toReadonly, + watch, } from '@vue/reactivity' - import { - FOR_ANCHOR_LABEL, - getSequence, - isArray, - isObject, - isString, - } from '@vue/shared' -import { isArray, isObject, isString } from '@vue/shared' ++import { FOR_ANCHOR_LABEL, isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' -import { - type Block, - VaporFragment, - insert, - remove as removeBlock, -} from './block' +import { type Block, insert, remove as removeBlock } from './block' import { warn } from '@vue/runtime-dom' import { currentInstance, isVaporComponent } from './component' import type { DynamicSlot } from './componentSlots' @@@ -96,24 -90,18 +93,30 @@@ export const createFor = let oldBlocks: ForBlock[] = [] let newBlocks: ForBlock[] let parent: ParentNode | undefined | null + // useSelector only + let currentKey: any - // TODO handle this in hydration - const parentAnchor = __DEV__ ? createComment('for') : createTextNode() + let parentAnchor: Node + if (isHydrating) { + parentAnchor = locateVaporFragmentAnchor( + currentHydrationNode!, + FOR_ANCHOR_LABEL, + )! + if (__DEV__ && !parentAnchor) { + // this should not happen + throw new Error(`v-for fragment anchor node was not found.`) + } + } else { + parentAnchor = __DEV__ ? createComment('for') : createTextNode() + } + const frag = new VaporFragment(oldBlocks) const instance = currentInstance! - const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE - const isComponent = flags & VaporVForFlags.IS_COMPONENT + const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE) + const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT) + const selectors: { + deregister: (key: any) => void + cleanup: () => void + }[] = [] if (__DEV__ && !instance) { warn('createFor() can only be used inside setup()') @@@ -181,104 -171,119 +190,119 @@@ } } - // 2. sync from end - // a (b c) - // d e (b c) - while (i <= e1 && i <= e2) { - if (tryPatchIndex(source, i)) { - e1-- - e2-- - } else { - break + const sharedBlockCount = Math.min(oldLength, newLength) + const previousKeyIndexPairs: [any, number][] = new Array(oldLength) + const queuedBlocks: [ + blockIndex: number, + blockItem: ReturnType, + blockKey: any, + ][] = new Array(newLength) + + let anchorFallback: Node = parentAnchor + let endOffset = 0 + let startOffset = 0 + let queuedBlocksInsertIndex = 0 + let previousKeyIndexInsertIndex = 0 + + while (endOffset < sharedBlockCount) { + const currentIndex = newLength - endOffset - 1 + const currentItem = getItem(source, currentIndex) + const currentKey = getKey(...currentItem) + const existingBlock = oldBlocks[oldLength - endOffset - 1] + if (existingBlock.key === currentKey) { + update(existingBlock, ...currentItem) + newBlocks[currentIndex] = existingBlock + endOffset++ + continue } + if (endOffset !== 0) { - anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes) ++ anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)! + } + break } - // 3. common sequence + mount - // (a b) - // (a b) c - // i = 2, e1 = 1, e2 = 2 - // (a b) - // c (a b) - // i = 0, e1 = -1, e2 = 0 - if (i > e1) { - if (i <= e2) { - const nextPos = e2 + 1 - const anchor = - nextPos < newLength - ? normalizeAnchor(newBlocks[nextPos].nodes) - : parentAnchor - while (i <= e2) { - mount(source, i, anchor) - i++ - } + while (startOffset < sharedBlockCount - endOffset) { + const currentItem = getItem(source, startOffset) + const currentKey = getKey(...currentItem) + const previousBlock = oldBlocks[startOffset] + const previousKey = previousBlock.key + if (previousKey === currentKey) { + update((newBlocks[startOffset] = previousBlock), currentItem[0]) + } else { + queuedBlocks[queuedBlocksInsertIndex++] = [ + startOffset, + currentItem, + currentKey, + ] + previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [ + previousKey, + startOffset, + ] } + startOffset++ } - // 4. common sequence + unmount - // (a b) c - // (a b) - // i = 2, e1 = 2, e2 = 1 - // a (b c) - // (b c) - // i = 0, e1 = 0, e2 = -1 - else if (i > e2) { - while (i <= e1) { - unmount(oldBlocks[i]) - i++ - } + for (let i = startOffset; i < oldLength - endOffset; i++) { + previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [ + oldBlocks[i].key, + i, + ] } - // 5. unknown sequence - // [i ... e1 + 1]: a b [c d e] f g - // [i ... e2 + 1]: a b [e d c h] f g - // i = 2, e1 = 4, e2 = 5 - else { - const s1 = i // prev starting index - const s2 = i // next starting index - - // 5.1 build key:index map for newChildren - const keyToNewIndexMap = new Map() - for (i = s2; i <= e2; i++) { - keyToNewIndexMap.set(getKey(...getItem(source, i)), i) - } + const preparationBlockCount = Math.min( + newLength - endOffset, + sharedBlockCount, + ) + for (let i = startOffset; i < preparationBlockCount; i++) { + const blockItem = getItem(source, i) + const blockKey = getKey(...blockItem) + queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey] + } - // 5.2 loop through old children left to be patched and try to patch - // matching nodes & remove nodes that are no longer present - let j - let patched = 0 - const toBePatched = e2 - s2 + 1 - let moved = false - // used to track whether any node has moved - let maxNewIndexSoFar = 0 - // works as Map - // Note that oldIndex is offset by +1 - // and oldIndex = 0 is a special value indicating the new node has - // no corresponding old node. - // used for determining longest stable subsequence - const newIndexToOldIndexMap = new Array(toBePatched).fill(0) - - for (i = s1; i <= e1; i++) { - const prevBlock = oldBlocks[i] - if (patched >= toBePatched) { - // all new children have been patched so this can only be a removal - unmount(prevBlock) + if (!queuedBlocksInsertIndex && !previousKeyIndexInsertIndex) { + for (let i = preparationBlockCount; i < newLength - endOffset; i++) { + const blockItem = getItem(source, i) + const blockKey = getKey(...blockItem) + mount(source, i, anchorFallback, blockItem, blockKey) + } + } else { + queuedBlocks.length = queuedBlocksInsertIndex + previousKeyIndexPairs.length = previousKeyIndexInsertIndex + + const previousKeyIndexMap = new Map(previousKeyIndexPairs) + const blocksToMount: [ + blockIndex: number, + blockItem: ReturnType, + blockKey: any, + anchorOffset: number, + ][] = [] + + const relocateOrMountBlock = ( + blockIndex: number, + blockItem: ReturnType, + blockKey: any, + anchorOffset: number, + ) => { + const previousIndex = previousKeyIndexMap.get(blockKey) + if (previousIndex !== undefined) { + const reusedBlock = (newBlocks[blockIndex] = + oldBlocks[previousIndex]) + update(reusedBlock, ...blockItem) + insert( + reusedBlock, + parent!, + anchorOffset === -1 + ? anchorFallback + : normalizeAnchor(newBlocks[anchorOffset].nodes), + ) + previousKeyIndexMap.delete(blockKey) } else { - const newIndex = keyToNewIndexMap.get(prevBlock.key) - if (newIndex == null) { - unmount(prevBlock) - } else { - newIndexToOldIndexMap[newIndex - s2] = i + 1 - if (newIndex >= maxNewIndexSoFar) { - maxNewIndexSoFar = newIndex - } else { - moved = true - } - update( - (newBlocks[newIndex] = prevBlock), - ...getItem(source, newIndex), - ) - patched++ - } + blocksToMount.push([ + blockIndex, + blockItem, + blockKey, + anchorOffset, + ]) } } @@@ -353,14 -384,9 +403,14 @@@ itemRef, keyRef, indexRef, - getKey && getKey(item, key, index), + key2, )) + // apply transition for new nodes + if (frag.$transition) { + applyTransitionHooks(block.nodes, frag.$transition, false) + } + if (parent) insert(block.nodes, parent, anchor) return block diff --cc packages/runtime-vapor/src/component.ts index cfda7ced9d,da57882c49..a74f690b34 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@@ -332,21 -289,36 +330,36 @@@ export function applyFallthroughProps */ export function devRender(instance: VaporComponentInstance): void { instance.block = - callWithErrorHandling( - instance.type.render!, - instance, - ErrorCodes.RENDER_FUNCTION, - [ - instance.setupState, - instance.props, - instance.emit, - instance.attrs, - instance.slots, - ], - ) || [] + (instance.type.render + ? callWithErrorHandling( + instance.type.render, + instance, + ErrorCodes.RENDER_FUNCTION, + [ + instance.setupState, + instance.props, + instance.emit, + instance.attrs, + instance.slots, + ], + ) + : callWithErrorHandling( + isFunction(instance.type) ? instance.type : instance.type.setup!, + instance, + ErrorCodes.SETUP_FUNCTION, + [ + instance.props, + { + slots: instance.slots, + attrs: instance.attrs, + emit: instance.emit, + expose: instance.expose, + }, + ], + )) || [] } -const emptyContext: GenericAppContext = { +export const emptyContext: GenericAppContext = { app: null as any, config: {}, provides: /*@__PURE__*/ Object.create(null), diff --cc packages/runtime-vapor/src/fragment.ts index 03c212f728,0000000000..ce74e07f74 mode 100644,000000..100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@@ -1,150 -1,0 +1,150 @@@ - import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' ++import { EffectScope, setActiveSub } from '@vue/reactivity' +import { createComment, createTextNode } from './dom/node' +import { + type Block, + type BlockFn, + type TransitionOptions, + type VaporTransitionHooks, + insert, + isValidBlock, + remove, +} from './block' +import type { TransitionHooks } from '@vue/runtime-dom' +import { + currentHydrationNode, + isComment, + isHydrating, + locateHydrationNode, + locateVaporFragmentAnchor, + setCurrentHydrationNode, +} from './dom/hydration' +import { + applyTransitionHooks, + applyTransitionLeaveHooks, +} from './components/Transition' +import type { VaporComponentInstance } from './component' + +export class VaporFragment implements TransitionOptions { + $key?: any + $transition?: VaporTransitionHooks | undefined + nodes: Block + anchor?: Node + insert?: ( + parent: ParentNode, + anchor: Node | null, + transitionHooks?: TransitionHooks, + ) => void + remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void + fallback?: BlockFn + + target?: ParentNode | null + targetAnchor?: Node | null + getNodes?: () => Block + setRef?: (comp: VaporComponentInstance) => void + + constructor(nodes: Block) { + this.nodes = nodes + } +} + +export class DynamicFragment extends VaporFragment { + anchor!: Node + scope: EffectScope | undefined + current?: BlockFn + fallback?: BlockFn + /** + * slot only + * indicates forwarded slot + */ + forwarded?: boolean + + constructor(anchorLabel?: string) { + super([]) + if (isHydrating) { + locateHydrationNode(anchorLabel === 'slot') + this.hydrate(anchorLabel!) + } else { + this.anchor = + __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() + } + } + + update(render?: BlockFn, key: any = render): void { + if (key === this.current) { + return + } + this.current = key + - pauseTracking() ++ const prevSub = setActiveSub() + const parent = this.anchor.parentNode + const transition = this.$transition + const renderBranch = () => { + if (render) { + this.scope = new EffectScope() + this.nodes = this.scope.run(render) || [] + if (transition) { + this.$transition = applyTransitionHooks(this.nodes, transition) + } + if (parent) insert(this.nodes, parent, this.anchor) + } else { + this.scope = undefined + this.nodes = [] + } + } + + // teardown previous branch + if (this.scope) { + this.scope.stop() + const mode = transition && transition.mode + if (mode) { + applyTransitionLeaveHooks(this.nodes, transition, renderBranch) + parent && remove(this.nodes, parent) + if (mode === 'out-in') { - resetTracking() ++ setActiveSub(prevSub) + return + } + } else { + parent && remove(this.nodes, parent) + } + } + + renderBranch() + + if (this.fallback && !isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent) + this.nodes = + (this.scope || (this.scope = new EffectScope())).run(this.fallback) || + [] + parent && insert(this.nodes, parent, this.anchor) + } + + if (isHydrating) { + setCurrentHydrationNode(this.anchor.nextSibling) + } - resetTracking() ++ setActiveSub(prevSub) + } + + hydrate(label: string): void { + // for `v-if="false"` the node will be an empty comment, use it as the anchor. + // otherwise, find next sibling vapor fragment anchor + if (label === 'if' && isComment(currentHydrationNode!, '')) { + this.anchor = currentHydrationNode + } else { + let anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)! + if (!anchor && (label === 'slot' || label === 'if')) { + // fallback to fragment end anchor for ssr vdom slot + anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')! + } + if (anchor) { + this.anchor = anchor + } else if (__DEV__) { + // this should not happen + throw new Error(`${label} fragment anchor node was not found.`) + } + } + } +} + +export function isFragment(val: NonNullable): val is VaporFragment { + return val instanceof VaporFragment +} diff --cc packages/runtime-vapor/src/hmr.ts index 63e5376896,c96c1afa13..17b1bd0f23 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@@ -20,12 -18,7 +19,11 @@@ export function hmrRerender(instance: V const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling remove(instance.block, parent) + if (instance.hmrRerenderEffects) { + instance.hmrRerenderEffects.forEach(e => e()) + instance.hmrRerenderEffects.length = 0 + } - const prev = currentInstance - simpleSetCurrentInstance(instance) + const prev = setCurrentInstance(instance) pushWarningContext(instance) devRender(instance) popWarningContext() @@@ -49,7 -41,6 +46,7 @@@ export function hmrReload instance.rawSlots, instance.isSingleRoot, ) - simpleSetCurrentInstance(prev, instance.parent) + setCurrentInstance(...prev) mountComponent(newInstance, parent, anchor) + handleTeleportRootComponentHmrReload(instance, newInstance) } diff --cc packages/runtime-vapor/src/index.ts index f02063da1c,7a8aea5a0d..0b7b5aaa6f --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@@ -1,17 -1,19 +1,21 @@@ // public APIs export { createVaporApp, createVaporSSRApp } from './apiCreateApp' export { defineVaporComponent } from './apiDefineComponent' +export { defineVaporAsyncComponent } from './apiDefineAsyncComponent' export { vaporInteropPlugin } from './vdomInterop' export type { VaporDirective } from './directives/custom' +export { VaporTeleportImpl as VaporTeleport } from './components/Teleport' // compiler-use only -export { insert, prepend, remove, isFragment, VaporFragment } from './block' +export { insert, prepend, remove } from './block' export { setInsertionState } from './insertionState' - export { createComponent, createComponentWithFallback } from './component' + export { + createComponent, + createComponentWithFallback, + isVaporComponent, + } from './component' export { renderEffect } from './renderEffect' -export { createSlot } from './componentSlots' +export { createSlot, forwardedSlotCreator } from './componentSlots' export { template } from './dom/template' export { createTextNode, child, nthChild, next } from './dom/node' export { diff --cc packages/runtime-vapor/src/renderEffect.ts index 227d7933e7,ac34e8863d..8317c2130c --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@@ -11,64 -11,81 +11,85 @@@ import import { type VaporComponentInstance, isVaporComponent } from './component' import { invokeArrayFns } from '@vue/shared' - export function renderEffect( - fn: () => void, - noLifecycle = false, - ): ReactiveEffect { - const instance = currentInstance as VaporComponentInstance | null - const scope = getCurrentScope() - if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) { - warn('renderEffect called without active EffectScope or Vapor instance.') - } + class RenderEffect extends ReactiveEffect { + i: VaporComponentInstance | null + job: SchedulerJob + updateJob: SchedulerJob + + constructor(public render: () => void) { + super() + const instance = currentInstance as VaporComponentInstance | null + if (__DEV__ && !__TEST__ && !this.subs && !isVaporComponent(instance)) { + warn('renderEffect called without active EffectScope or Vapor instance.') + } - // renderEffect is always called after user has registered all hooks - const hasUpdateHooks = instance && (instance.bu || instance.u) - const renderEffectFn = noLifecycle - ? fn - : () => { - if (__DEV__ && instance) { - startMeasure(instance, `renderEffect`) - } - const prev = currentInstance - simpleSetCurrentInstance(instance) - if (scope) scope.on() - if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) { - instance.isUpdating = true - instance.bu && invokeArrayFns(instance.bu) - fn() - queuePostFlushCb(() => { - instance.isUpdating = false - instance.u && invokeArrayFns(instance.u) - }) - } else { - fn() - } - if (scope) scope.off() - simpleSetCurrentInstance(prev, instance) - if (__DEV__ && instance) { - startMeasure(instance, `renderEffect`) - } + const job: SchedulerJob = () => { + if (this.dirty) { + this.run() } + } + this.updateJob = () => { + instance!.isUpdating = false + instance!.u && invokeArrayFns(instance!.u) + } + + if (instance) { + if (__DEV__) { + this.onTrack = instance.rtc + ? e => invokeArrayFns(instance.rtc!, e) + : void 0 + this.onTrigger = instance.rtg + ? e => invokeArrayFns(instance.rtg!, e) + : void 0 + } + job.i = instance + } + + this.job = job + this.i = instance - const effect = new ReactiveEffect(renderEffectFn) - const job: SchedulerJob = () => effect.dirty && effect.run() + // TODO recurse handling + } - if (instance) { - if (__DEV__) { - effect.onTrack = instance.rtc - ? e => invokeArrayFns(instance.rtc!, e) - : void 0 - effect.onTrigger = instance.rtg - ? e => invokeArrayFns(instance.rtg!, e) - : void 0 + fn(): void { + const instance = this.i + const scope = this.subs ? (this.subs.sub as EffectScope) : undefined + // renderEffect is always called after user has registered all hooks + const hasUpdateHooks = instance && (instance.bu || instance.u) + if (__DEV__ && instance) { + startMeasure(instance, `renderEffect`) + } + const prev = setCurrentInstance(instance, scope) + if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) { + instance.isUpdating = true + instance.bu && invokeArrayFns(instance.bu) + this.render() + queuePostFlushCb(this.updateJob) + } else { + this.render() + } + setCurrentInstance(...prev) + if (__DEV__ && instance) { + startMeasure(instance, `renderEffect`) } - job.i = instance - job.id = instance.uid } - effect.scheduler = () => queueJob(job) - effect.run() + notify(): void { + const flags = this.flags + if (!(flags & EffectFlags.PAUSED)) { + queueJob(this.job, this.i ? this.i.uid : undefined) + } + } + } -export function renderEffect(fn: () => void, noLifecycle = false): void { ++export function renderEffect( ++ fn: () => void, ++ noLifecycle = false, ++): RenderEffect { + const effect = new RenderEffect(fn) + if (noLifecycle) { + effect.fn = fn + } + effect.run() + return effect - // TODO recurse handling } diff --cc packages/runtime-vapor/src/vdomInterop.ts index 6b94261f1d,1573a30692..e02b3d0cfe --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@@ -48,19 -34,11 +48,21 @@@ import import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' -import { createTextNode } from './dom/node' +import { __next, createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' +import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition' +import { + currentHydrationNode, + isHydrating, + locateHydrationNode, + locateVaporFragmentAnchor, + setCurrentHydrationNode, + hydrateNode as vaporHydrateNode, +} from './dom/hydration' +import { DynamicFragment, VaporFragment, isFragment } from './fragment' + export const interopKey: unique symbol = Symbol(`interop`) + // mounting vapor components and slots in vdom const vaporInteropImpl: Omit< VaporInteropInterface, @@@ -72,17 -50,14 +74,22 @@@ const prev = currentInstance simpleSetCurrentInstance(parentComponent) - const propsRef = shallowRef(vnode.props) + // filter out reserved props + const props: VNode['props'] = {} + for (const key in vnode.props) { + if (!isReservedProp(key)) { + props[key] = vnode.props[key] + } + } + + const propsRef = shallowRef(props) const slotsRef = shallowRef(vnode.children) + const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [ + () => propsRef.value, + ] + // mark as interop props + dynamicPropSource[interopKey] = true // @ts-expect-error const instance = (vnode.component = createComponent( vnode.type as any as VaporComponent, diff --cc packages/shared/src/index.ts index 674bcdf96c,0c38d640ba..9372b8e1a9 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@@ -13,4 -13,4 +13,5 @@@ export * from './looseEqual export * from './toDisplayString' export * from './typeUtils' export * from './subSequence' +export * from './domAnchors' + export * from './cssVars'