]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
chore: Merge branch 'minor' into edison/testVapor
authordaiwei <daiwei521@126.com>
Tue, 29 Jul 2025 05:59:41 +0000 (13:59 +0800)
committerdaiwei <daiwei521@126.com>
Tue, 29 Jul 2025 05:59:41 +0000 (13:59 +0800)
22 files changed:
1  2 
packages/compiler-ssr/__tests__/ssrVModel.spec.ts
packages/compiler-vapor/src/generators/block.ts
packages/compiler-vapor/src/generators/component.ts
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/apiAsyncComponent.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/components/Teleport.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/src/apiCreateApp.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiTemplateRef.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/hmr.ts
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/renderEffect.ts
packages/runtime-vapor/src/vdomInterop.ts
packages/shared/src/index.ts

index c88f6ba3182bab47d3cd1108096d9fd81e57918c,8a439dbf4b5cc6063d0a74d7f1859539b1d8082a..f7b7a3241b22042423b22155a3b749523a9259b7
@@@ -167,6 -166,132 +167,135 @@@ describe('ssr: v-model', () => 
          _push(\`</optgroup></select></div>\`)
        }"
      `)
 -        _push(\`<!--]--></optgroup></select></div>\`)
+     expect(
+       compileWithWrapper(`
+         <select multiple v-model="model">
+           <optgroup>
+             <option v-for="item in items" :value="item">{{item}}</option>
+           </optgroup>
+         </select>`).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(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup><!--[-->\`)
+         _ssrRenderList(_ctx.items, (item) => {
+           _push(\`<option\${
+             _ssrRenderAttr("value", item)
+           }\${
+             (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+               ? _ssrLooseContain(_ctx.model, item)
+               : _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
+           }>\${
+             _ssrInterpolate(item)
+           }</option>\`)
+         })
 -          _push(\`<!--]-->\`)
++        _push(\`<!--]--><!--for--></optgroup></select></div>\`)
+       }"
+     `)
+     expect(
+       compileWithWrapper(`
+         <select multiple v-model="model">
+           <optgroup>
+             <option v-if="true" :value="item">{{item}}</option>
+           </optgroup>
+         </select>`).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(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup>\`)
+         if (true) {
+           _push(\`<option\${
+             _ssrRenderAttr("value", _ctx.item)
+           }\${
+             (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+               ? _ssrLooseContain(_ctx.model, _ctx.item)
+               : _ssrLooseEqual(_ctx.model, _ctx.item))) ? " selected" : ""
+           }>\${
+             _ssrInterpolate(_ctx.item)
+           }</option>\`)
++          _push(\`<!--if-->\`)
+         } else {
+           _push(\`<!---->\`)
+         }
+         _push(\`</optgroup></select></div>\`)
+       }"
+     `)
+     expect(
+       compileWithWrapper(`
+         <select multiple v-model="model">
+           <optgroup>
+             <template v-if="ok">
+               <option v-for="item in items" :value="item">{{item}}</option>
+             </template>
+           </optgroup>
+         </select>`).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(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup>\`)
+         if (_ctx.ok) {
+           _push(\`<!--[-->\`)
+           _ssrRenderList(_ctx.items, (item) => {
+             _push(\`<option\${
+               _ssrRenderAttr("value", item)
+             }\${
+               (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+                 ? _ssrLooseContain(_ctx.model, item)
+                 : _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
+             }>\${
+               _ssrInterpolate(item)
+             }</option>\`)
+           })
 -        _push(\`<!--]--></optgroup></select></div>\`)
++          _push(\`<!--]--><!--for-->\`)
++          _push(\`<!--if-->\`)
+         } else {
+           _push(\`<!---->\`)
+         }
+         _push(\`</optgroup></select></div>\`)
+       }"
+     `)
+     expect(
+       compileWithWrapper(`
+         <select multiple v-model="model">
+           <optgroup>
+             <template v-for="item in items" :value="item">
+               <option v-if="item===1" :value="item">{{item}}</option>
+             </template>
+           </optgroup>
+         </select>`).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(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup><!--[-->\`)
+         _ssrRenderList(_ctx.items, (item) => {
+           _push(\`<!--[-->\`)
+           if (item===1) {
+             _push(\`<option\${
+               _ssrRenderAttr("value", item)
+             }\${
+               (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+                 ? _ssrLooseContain(_ctx.model, item)
+                 : _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
+             }>\${
+               _ssrInterpolate(item)
+             }</option>\`)
++            _push(\`<!--if-->\`)
+           } else {
+             _push(\`<!---->\`)
+           }
+           _push(\`<!--]-->\`)
+         })
++        _push(\`<!--]--><!--for--></optgroup></select></div>\`)
+       }"
+     `)
    })
  
    test('<input type="radio">', () => {
index 0811921bd9655f2f9e61a913ce0495a80c312393,30347394756b90e7c63a385a91dc745597b43f4c..d101962ba4faea3ef52b42148c2513cf22fa0582
@@@ -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')
    }
  
    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}`)
index 8eee833ac4bf0976f1d5b4a07227877a54c093c8,5c5c06c4891c67fc588a189d28b0cc19014fd8a3..8841f669b7c53aed93eb7cc9b282c120b554c789
@@@ -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())
index 2e70778fe9668cad7fe573cb80ecd13df15aafca,a1409a7fe442e2247b357e40a5449075f95ec471..43c96e5003ad45223904bb94cf93fbdbd43110ff
@@@ -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'
index 62bcb7686744738d359ea459a6130cb5d96dd78a,15b3c7512bebb18395ff33ddb5d2c082a6e4d299..5db9a3bdd5218bfacf02223643853f47ea081ec8
@@@ -32,8 -31,8 +32,9 @@@ import 
    isRenderableAttrValue,
    isReservedProp,
    isString,
 +  isVaporAnchors,
    normalizeClass,
+   normalizeCssVarValue,
    normalizeStyle,
    stringifyStyle,
  } from '@vue/shared'
Simple merge
index b968255aa58ca9fd747e804444104edb521f71ae,30f9e6b2c1f08838ae929761e80f823d46dad88a..d3c2972a7fd9c1075060d7ed6ce55323aa523735
@@@ -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 {
Simple merge
index f609543afc8284503c91aae617d58014791f91fe,dbc9ebc83195f996fc8d2e0eb81f3d050a9d6189..5bde0790c8f446df4bed2a15930a2e7998752e01
@@@ -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
 -  // TODO handle this in hydration
 -  const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+   // useSelector only
+   let currentKey: any
 +  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()')
            }
          }
  
-         // 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<typeof getItem>,
+           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
            }
 -            anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)
+           if (endOffset !== 0) {
++            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<newIndex, oldIndex>
-           // 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<typeof getItem>,
+             blockKey: any,
+             anchorOffset: number,
+           ][] = []
+           const relocateOrMountBlock = (
+             blockIndex: number,
+             blockItem: ReturnType<typeof getItem>,
+             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,
+               ])
              }
            }
  
        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
index cfda7ced9d692a06ea3514d11cfd549f9b525d50,da57882c49de648cd4abdda7c133af7cc390b262..a74f690b34cb7b44fdf4265d69b974f534312188
@@@ -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),
index 03c212f728ea47c64a8c45597af01c5269684889,0000000000000000000000000000000000000000..ce74e07f74ff8512ff376a9de1ad59bd5aa7b68d
mode 100644,000000..100644
--- /dev/null
@@@ -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<unknown>): val is VaporFragment {
 +  return val instanceof VaporFragment
 +}
index 63e5376896a2fa63ae020aad7682a16abe487e5b,c96c1afa13054e1230f9cef57a5fa9d9d04e5c19..17b1bd0f23794f99968b2a1881bf349620be147e
@@@ -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)
-   const prev = currentInstance
-   simpleSetCurrentInstance(instance)
 +  if (instance.hmrRerenderEffects) {
 +    instance.hmrRerenderEffects.forEach(e => e())
 +    instance.hmrRerenderEffects.length = 0
 +  }
+   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)
  }
index f02063da1cadffaa2804b9c1ee6d1e778cad1214,7a8aea5a0d71778852b4eb97a8b470016c30340e..0b7b5aaa6f00294c8b09c0a5a65c8e8f79f67177
@@@ -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 {
index 227d7933e78eac3ee45a74f7e8deb6e5ec11e2db,ac34e8863d2ab8aada626553ef75863480887014..8317c2130c3428f61f081aa318c2188edbd9162e
@@@ -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<void> {
-   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
  }
index 6b94261f1da16e8f3f9a28e39a74129fe65a421b,1573a306922aacd6813fc8cdfe8f7c7a84ec4f4a..e02b3d0cfefadf5cc4c5fde16650a2270d7b397f
@@@ -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,
      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,
index 674bcdf96cd70277532ff858ff7d281af70ced5a,0c38d640ba0982f7a8439279624ca9f71018042a..9372b8e1a96ab3f6acdf89c968bf61f18d994152
@@@ -13,4 -13,4 +13,5 @@@ export * from './looseEqual
  export * from './toDisplayString'
  export * from './typeUtils'
  export * from './subSequence'
 +export * from './domAnchors'
+ export * from './cssVars'