_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">', () => {
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}`)
__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())
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'
isRenderableAttrValue,
isReservedProp,
isString,
+ isVaporAnchors,
normalizeClass,
+ normalizeCssVarValue,
normalizeStyle,
stringifyStyle,
} from '@vue/shared'
}
// #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,
+ )
}
}
}
}
-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 {
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'
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
*/
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),
--- /dev/null
- 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
+}
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()
instance.rawSlots,
instance.isSingleRoot,
)
- simpleSetCurrentInstance(prev, instance.parent)
+ setCurrentInstance(...prev)
mountComponent(newInstance, parent, anchor)
+ handleTeleportRootComponentHmrReload(instance, newInstance)
}
// 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 {
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
}
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,
export * from './toDisplayString'
export * from './typeUtils'
export * from './subSequence'
+export * from './domAnchors'
+ export * from './cssVars'