diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 3050f401..a01a1c66 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -116,8 +116,7 @@ export interface RendererOptions< parent: HostElement, anchor: HostNode | null, isSVG: boolean - ): HostElement - setStaticContent?(node: HostElement, content: string): void + ): HostElement[] } // Renderer Node can technically be any object in the context of core renderer @@ -333,8 +332,7 @@ function baseCreateRenderer( nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, - insertStaticContent: hostInsertStaticContent, - setStaticContent: hostSetStaticContent + insertStaticContent: hostInsertStaticContent } = options // Note: functions inside this closure should use `const xxx = () => {}` @@ -373,11 +371,7 @@ function baseCreateRenderer( if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { - // static nodes are only patched during dev for HMR - n2.el = n1.el - if (n2.children !== n1.children) { - hostSetStaticContent!(n2.el!, n2.children as string) - } + patchStaticNode(n1, n2, container, isSVG) } break case Fragment: @@ -492,11 +486,19 @@ function baseCreateRenderer( isSVG: boolean ) => { if (n2.el && hostCloneNode !== undefined) { - hostInsert(hostCloneNode(n2.el), container, anchor) + // static node was already mounted (and reused), or adopted + // server-rendered node during hydration (in this case its children can be + // stripped by SSR optimizations). Clone the dom nodes instead. + let cur: RendererElement | null = n2.el + while (cur && cur !== n2.anchor) { + hostInsert(hostCloneNode(cur), container, anchor) + cur = hostNextSibling(cur) + } + hostInsert(hostCloneNode(n2.anchor!), container, anchor) } else { // static nodes are only present when used with compiler-dom/runtime-dom // which guarantees presence of hostInsertStaticContent. - n2.el = hostInsertStaticContent!( + ;[n2.el, n2.anchor] = hostInsertStaticContent!( n2.children as string, container, anchor, @@ -505,6 +507,64 @@ function baseCreateRenderer( } } + /** + * Dev / HMR only + */ + const patchStaticNode = ( + n1: VNode, + n2: VNode, + container: RendererElement, + isSVG: boolean + ) => { + // static nodes are only patched during dev for HMR + if (n2.children !== n1.children) { + const anchor = hostNextSibling(n1.anchor!) + // remove existing + removeStaticNode(n1) + // insert new + ;[n2.el, n2.anchor] = hostInsertStaticContent!( + n2.children as string, + container, + anchor, + isSVG + ) + } else { + n2.el = n1.el + n2.anchor = n1.anchor + } + } + + /** + * Dev / HMR only + */ + const moveStaticNode = ( + vnode: VNode, + container: RendererElement, + anchor: RendererNode | null + ) => { + let cur = vnode.el + const end = vnode.anchor! + while (cur && cur !== end) { + const next = hostNextSibling(cur) + hostInsert(cur, container, anchor) + cur = next + } + hostInsert(end, container, anchor) + } + + /** + * Dev / HMR only + */ + const removeStaticNode = (vnode: VNode) => { + let cur = vnode.el + while (cur && cur !== vnode.anchor) { + const next = hostNextSibling(cur) + hostRemove(cur) + cur = next + } + hostRemove(vnode.anchor!) + } + const processElement = ( n1: VNode | null, n2: VNode, @@ -1456,7 +1516,7 @@ function baseCreateRenderer( n1, n2, container, - parentAnchor, + null, parentComponent, parentSuspense, isSVG, @@ -1481,7 +1541,7 @@ function baseCreateRenderer( n1, n2, container, - parentAnchor, + null, parentComponent, parentSuspense, isSVG, @@ -1692,6 +1752,12 @@ function baseCreateRenderer( return } + // static node move can only happen when force updating HMR + if (__DEV__ && type === Static) { + moveStaticNode(vnode, container, anchor) + return + } + // single nodes const needTransition = moveType !== MoveType.REORDER && @@ -1808,6 +1874,11 @@ function baseCreateRenderer( return } + if (__DEV__ && type === Static) { + removeStaticNode(vnode) + return + } + const performRemove = () => { hostRemove(el!) if (transition && !transition.persisted && transition.afterLeave) { diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 108e516a..7be2a109 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -127,6 +127,7 @@ export interface VNode { anchor: HostNode | null // fragment anchor target: HostElement | null // teleport target targetAnchor: HostNode | null // teleport target anchor + staticCount: number // number of elements contained in a static vnode // optimization only shapeFlag: number @@ -368,6 +369,7 @@ function _createVNode( anchor: null, target: null, targetAnchor: null, + staticCount: 0, shapeFlag, patchFlag, dynamicProps, @@ -422,6 +424,7 @@ export function cloneVNode( children: vnode.children, target: vnode.target, targetAnchor: vnode.targetAnchor, + staticCount: vnode.staticCount, shapeFlag: vnode.shapeFlag, // if the vnode is cloned with extra props, we can no longer assume its // existing patch flag to be reliable and need to bail out of optimized mode. @@ -459,8 +462,15 @@ export function createTextVNode(text: string = ' ', flag: number = 0): VNode { /** * @internal */ -export function createStaticVNode(content: string): VNode { - return createVNode(Static, null, content) +export function createStaticVNode( + content: string, + numberOfNodes: number +): VNode { + // A static vnode can contain multiple stringified elements, and the number + // of elements is necessary for hydration. + const vnode = createVNode(Static, null, content) + vnode.staticCount = numberOfNodes + return vnode } /** diff --git a/packages/runtime-dom/src/nodeOps.ts b/packages/runtime-dom/src/nodeOps.ts index 42e7ee03..6d766385 100644 --- a/packages/runtime-dom/src/nodeOps.ts +++ b/packages/runtime-dom/src/nodeOps.ts @@ -64,17 +64,14 @@ export const nodeOps: Omit, 'patchProp'> = { (tempSVGContainer = doc.createElementNS(svgNS, 'svg')) : tempContainer || (tempContainer = doc.createElement('div')) temp.innerHTML = content - const node = temp.children[0] - nodeOps.insert(node, parent, anchor) - return node - } -} - -if (__DEV__) { - // __UNSAFE__ - // Reason: innerHTML. - // same as `insertStaticContent`, but this is also dev only (for HMR). - nodeOps.setStaticContent = (el, content) => { - el.innerHTML = content + const first = temp.firstChild as Element + let node: Element | null = first + let last: Element = node + while (node) { + last = node + nodeOps.insert(node, parent, anchor) + node = temp.firstChild as Element + } + return [first, last] } }