From 9f51f1303b20f58a0b132dc77ab8f119ee50c20f Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 8 May 2025 11:58:42 +0800 Subject: [PATCH] wip: save --- .../runtime-core/src/helpers/renderSlot.ts | 1 - packages/runtime-core/src/renderer.ts | 6 +- packages/runtime-core/src/vnode.ts | 5 - packages/runtime-dom/src/apiCustomElement.ts | 129 +++++++++++++++--- 4 files changed, 113 insertions(+), 28 deletions(-) diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index aec49d1eae..92f7dab36b 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -96,7 +96,6 @@ export function renderSlot( if (slot && (slot as ContextualRenderFn)._c) { ;(slot as ContextualRenderFn)._d = true } - rendered.slotName = name return rendered } diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index a4f533c487..e77f624c4f 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -936,7 +936,7 @@ function baseCreateRenderer( } if (el._isVueCE && el._def.shadowRoot === false) { - el._updateSlots(n2.children) + el._updateSlots(n1, n2) } } @@ -966,7 +966,9 @@ function baseCreateRenderer( !isSameVNodeType(oldVNode, newVNode) || // - In the case of a component, it could contain anything. oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT)) - ? hostParentNode(oldVNode.el)! + ? oldVNode.el._parentNode && !oldVNode.el.isConnected + ? oldVNode.el._parentNode + : hostParentNode(oldVNode.el)! : // In other cases, the parent container is not actually used so we // just pass the block element here to avoid a DOM parentNode call. fallbackContainer diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index a146cdc162..a8c5340cd1 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -253,10 +253,6 @@ export interface VNode< * @internal custom element interception hook */ ce?: (instance: ComponentInternalInstance) => void - /** - * @internal - */ - slotName?: string } // Since v-if and v-for are the two possible ways node structure can dynamically @@ -719,7 +715,6 @@ export function cloneVNode( anchor: vnode.anchor, ctx: vnode.ctx, ce: vnode.ce, - slotName: vnode.slotName, } // if the vnode will be replaced by the cloned one, it is necessary diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index ebd75b252a..41d4dc5030 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -245,6 +245,8 @@ export class VueElement private _childStyles?: Map private _ob?: MutationObserver | null = null private _slots?: Record + private _slotFallbacks?: Record + private _slotAnchors?: Map constructor( /** @@ -529,8 +531,11 @@ export class VueElement private _createVNode(): VNode { const baseProps: VNodeProps = {} if (!this.shadowRoot) { - baseProps.onVnodeMounted = baseProps.onVnodeUpdated = - this._renderSlots.bind(this) + baseProps.onVnodeMounted = () => { + this._captureSlotFallbacks() + this._renderSlots() + } + baseProps.onVnodeUpdated = this._renderSlots.bind(this) } const vnode = createVNode(this._def, extend(baseProps, this._props)) if (!this._instance) { @@ -617,14 +622,19 @@ export class VueElement /** * Only called when shadowRoot is false */ - private _parseSlots() { + private _parseSlots(remove: boolean = true) { const slots: VueElement['_slots'] = (this._slots = {}) - let n - while ((n = this.firstChild)) { + let n = this.firstChild + while (n) { const slotName = (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default' ;(slots[slotName] || (slots[slotName] = [])).push(n) - this.removeChild(n) + const next = n.nextSibling + // store the parentNode reference since node will be removed + // but it is needed during patching + ;(n as any)._parentNode = n.parentNode + if (remove) this.removeChild(n) + n = next } } @@ -634,11 +644,18 @@ export class VueElement private _renderSlots() { const outlets = (this._teleportTarget || this).querySelectorAll('slot') const scopeId = this._instance!.type.__scopeId + this._slotAnchors = new Map() for (let i = 0; i < outlets.length; i++) { const o = outlets[i] as HTMLSlotElement const slotName = o.getAttribute('name') || 'default' const content = this._slots![slotName] const parent = o.parentNode! + + // insert an anchor to facilitate updates + const anchor = document.createTextNode('') + this._slotAnchors.set(slotName, anchor) + parent.insertBefore(anchor, o) + if (content) { for (const n of content) { // for :slotted css @@ -651,23 +668,91 @@ export class VueElement ;(child as Element).setAttribute(id, '') } } - parent.insertBefore(n, o) + parent.insertBefore(n, anchor) + } + } else if (this._slotFallbacks) { + const nodes = this._slotFallbacks[slotName] + if (nodes) { + for (const n of nodes) { + parent.insertBefore(n, anchor) + } } - } else { - while (o.firstChild) parent.insertBefore(o.firstChild, o) } parent.removeChild(o) } } /** - * @internal + * Only called when shadowRoot is false */ - _updateSlots(children: VNode[]): void { - children.forEach(child => { - // slot children are always Fragments - this._slots![child.slotName!] = collectFragmentElements(child) - }) + _updateSlots(n1: VNode, n2: VNode): void { + // replace v-if nodes + const prevNodes = collectNodes(n1.children as VNodeArrayChildren) + const newNodes = collectNodes(n2.children as VNodeArrayChildren) + for (let i = 0; i < prevNodes.length; i++) { + const prevNode = prevNodes[i] + const newNode = newNodes[i] + if (isComment(prevNode, 'v-if') || isComment(newNode, 'v-if')) { + Object.keys(this._slots!).forEach(name => { + const slotNodes = this._slots![name] + if (slotNodes) { + for (const node of slotNodes) { + if (node === prevNode) { + this._slots![name][i] = newNode + break + } + } + } + }) + } + } + + // switch between fallback and provided content + if (this._slotFallbacks) { + const oldSlotNames = Object.keys(this._slots!) + // re-parse slots + this._parseSlots(false) + const newSlotNames = Object.keys(this._slots!) + const allSlotNames = new Set([...oldSlotNames, ...newSlotNames]) + allSlotNames.forEach(name => { + const fallbackNodes = this._slotFallbacks![name] + if (fallbackNodes) { + // render fallback nodes for removed slots + if (!newSlotNames.includes(name)) { + const anchor = this._slotAnchors!.get(name)! + fallbackNodes.forEach(fallbackNode => + this.insertBefore(fallbackNode, anchor), + ) + } + + // remove fallback nodes for added slots + if (!oldSlotNames.includes(name)) { + fallbackNodes.forEach(fallbackNode => + this.removeChild(fallbackNode), + ) + } + } + }) + } + } + + /** + * Only called when shadowRoot is false + */ + private _captureSlotFallbacks() { + const outlets = (this._teleportTarget || this).querySelectorAll('slot') + for (let i = 0; i < outlets.length; i++) { + const slotElement = outlets[i] as HTMLSlotElement + const slotName = slotElement.getAttribute('name') || 'default' + const fallbackNodes: Node[] = [] + while (slotElement.firstChild) { + fallbackNodes.push(slotElement.removeChild(slotElement.firstChild)) + } + if (fallbackNodes.length) { + ;(this._slotFallbacks || (this._slotFallbacks = {}))[slotName] = + fallbackNodes + } + } } /** @@ -724,22 +809,22 @@ export function useShadowRoot(): ShadowRoot | null { return el && el.shadowRoot } -function collectFragmentElements(child: VNode): Node[] { +function collectFragmentNodes(child: VNode): Node[] { return [ child.el as Node, - ...collectElements(child.children as VNodeArrayChildren), + ...collectNodes(child.children as VNodeArrayChildren), child.anchor as Node, ] } -function collectElements(children: VNodeArrayChildren): Node[] { +function collectNodes(children: VNodeArrayChildren): Node[] { const nodes: Node[] = [] for (const child of children) { if (isArray(child)) { - nodes.push(...collectElements(child)) + nodes.push(...collectNodes(child)) } else if (isVNode(child)) { if (child.type === Fragment) { - nodes.push(...collectFragmentElements(child)) + nodes.push(...collectFragmentNodes(child)) } else if (child.el) { nodes.push(child.el as Node) } @@ -747,3 +832,7 @@ function collectElements(children: VNodeArrayChildren): Node[] { } return nodes } + +function isComment(node: Node, data: string): node is Comment { + return node.nodeType === 8 && (node as Comment).data === data +} -- 2.47.2