From ba89ca9ecafe86292e3adf751671ed5e9ca6e928 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 1 Jul 2021 19:17:07 -0400 Subject: [PATCH] fix(runtime-dom): fix static node content caching edge cases reverts fded1e8 fix #4023, #4031, #4037 --- packages/runtime-core/src/hydration.ts | 4 +- packages/runtime-core/src/renderer.ts | 16 +++++--- packages/runtime-core/src/vnode.ts | 5 ++- .../runtime-dom/__tests__/nodeOps.spec.ts | 40 ++++++++----------- packages/runtime-dom/src/nodeOps.ts | 33 +++++++-------- 5 files changed, 48 insertions(+), 50 deletions(-) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 5f1ed828b2c..50274fcb489 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -134,10 +134,10 @@ export function createHydrationFunctions( // if the static vnode has its content stripped during build, // adopt it from the server-rendered HTML. const needToAdoptContent = !(vnode.children as string).length - for (let i = 0; i < vnode.staticCount; i++) { + for (let i = 0; i < vnode.staticCount!; i++) { if (needToAdoptContent) vnode.children += (nextNode as Element).outerHTML - if (i === vnode.staticCount - 1) { + if (i === vnode.staticCount! - 1) { vnode.anchor = nextNode } nextNode = nextSibling(nextNode)! diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d869cadd68e..dd2661ead71 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -143,7 +143,7 @@ export interface RendererOptions< parent: HostElement, anchor: HostNode | null, isSVG: boolean, - cached?: [HostNode, HostNode | null] | null + cached?: HostNode[] | null ): HostElement[] } @@ -633,7 +633,7 @@ function baseCreateRenderer( ) => { // static nodes are only present when used with compiler-dom/runtime-dom // which guarantees presence of hostInsertStaticContent. - ;[n2.el, n2.anchor] = hostInsertStaticContent!( + const nodes = hostInsertStaticContent!( n2.children as string, container, anchor, @@ -641,8 +641,14 @@ function baseCreateRenderer( // pass cached nodes if the static node is being mounted multiple times // so that runtime-dom can simply cloneNode() instead of inserting new // HTML - n2.el && [n2.el, n2.anchor] + n2.staticCache ) + // first mount - this is the orignal hoisted vnode. cache nodes. + if (!n2.el) { + n2.staticCache = nodes + } + n2.el = nodes[0] + n2.anchor = nodes[nodes.length - 1] } /** @@ -686,16 +692,14 @@ function baseCreateRenderer( hostInsert(anchor!, container, nextSibling) } - const removeStaticNode = (vnode: VNode) => { + const removeStaticNode = ({ el, anchor }: VNode) => { let next - let { el, anchor } = vnode while (el && el !== anchor) { next = hostNextSibling(el) hostRemove(el) el = next } hostRemove(anchor!) - vnode.el = vnode.anchor = null } const processElement = ( diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index ec5a8f5dc61..366340d9976 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -167,7 +167,8 @@ 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 + staticCount?: number // number of elements contained in a static vnode + staticCache?: HostNode[] // cache of parsed static nodes for faster repeated insertions // suspense suspense: SuspenseBoundary | null @@ -439,7 +440,6 @@ function _createVNode( anchor: null, target: null, targetAnchor: null, - staticCount: 0, shapeFlag, patchFlag, dynamicProps, @@ -521,6 +521,7 @@ export function cloneVNode( target: vnode.target, targetAnchor: vnode.targetAnchor, staticCount: vnode.staticCount, + staticCache: vnode.staticCache, 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 add the FULL_PROPS flag. diff --git a/packages/runtime-dom/__tests__/nodeOps.spec.ts b/packages/runtime-dom/__tests__/nodeOps.spec.ts index 5e625ff53c4..aef6564ad8b 100644 --- a/packages/runtime-dom/__tests__/nodeOps.spec.ts +++ b/packages/runtime-dom/__tests__/nodeOps.spec.ts @@ -30,15 +30,11 @@ describe('runtime-dom: node-ops', () => { test('fresh insertion', () => { const content = `
one
two
three` const parent = document.createElement('div') - const [first, last] = nodeOps.insertStaticContent!( - content, - parent, - null, - false - ) + const nodes = nodeOps.insertStaticContent!(content, parent, null, false) expect(parent.innerHTML).toBe(content) - expect(first).toBe(parent.firstChild) - expect(last).toBe(parent.lastChild) + expect(nodes.length).toBe(3) + expect(nodes[0]).toBe(parent.firstChild) + expect(nodes[nodes.length - 1]).toBe(parent.lastChild) }) test('fresh insertion with anchor', () => { @@ -47,15 +43,13 @@ describe('runtime-dom: node-ops', () => { const parent = document.createElement('div') parent.innerHTML = existing const anchor = parent.firstChild - const [first, last] = nodeOps.insertStaticContent!( - content, - parent, - anchor, - false - ) + const nodes = nodeOps.insertStaticContent!(content, parent, anchor, false) expect(parent.innerHTML).toBe(content + existing) - expect(first).toBe(parent.firstChild) - expect(last).toBe(parent.childNodes[parent.childNodes.length - 2]) + expect(nodes.length).toBe(3) + expect(nodes[0]).toBe(parent.firstChild) + expect(nodes[nodes.length - 1]).toBe( + parent.childNodes[parent.childNodes.length - 2] + ) }) test('fresh insertion as svg', () => { @@ -97,7 +91,7 @@ describe('runtime-dom: node-ops', () => { const content = `
one
two
three` const cacheParent = document.createElement('div') - const [cachedFirst, cachedLast] = nodeOps.insertStaticContent!( + const nodes = nodeOps.insertStaticContent!( content, cacheParent, null, @@ -106,20 +100,18 @@ describe('runtime-dom: node-ops', () => { const parent = document.createElement('div') - const [first, last] = nodeOps.insertStaticContent!( + const clonedNodes = nodeOps.insertStaticContent!( ``, parent, null, false, - [cachedFirst, cachedLast] + nodes ) expect(parent.innerHTML).toBe(content) - expect(first).toBe(parent.firstChild) - expect(last).toBe(parent.lastChild) - - expect(first).not.toBe(cachedFirst) - expect(last).not.toBe(cachedLast) + expect(clonedNodes[0]).toBe(parent.firstChild) + expect(clonedNodes[clonedNodes.length - 1]).toBe(parent.lastChild) + expect(clonedNodes[0]).not.toBe(nodes[0]) }) }) }) diff --git a/packages/runtime-dom/src/nodeOps.ts b/packages/runtime-dom/src/nodeOps.ts index 087725aae3a..ceccf13b8b6 100644 --- a/packages/runtime-dom/src/nodeOps.ts +++ b/packages/runtime-dom/src/nodeOps.ts @@ -73,17 +73,15 @@ export const nodeOps: Omit, 'patchProp'> = { // As long as the user only uses trusted templates, this is safe. insertStaticContent(content, parent, anchor, isSVG, cached) { if (cached) { - let [cachedFirst, cachedLast] = cached - let first, last - while (true) { - let node = cachedFirst.cloneNode(true) - if (!first) first = node + let first + let last + let i = 0 + let l = cached.length + for (; i < l; i++) { + const node = cached[i].cloneNode(true) + if (i === 0) first = node + if (i === l - 1) last = node parent.insertBefore(node, anchor) - if (cachedFirst === cachedLast) { - last = node - break - } - cachedFirst = cachedFirst.nextSibling! } return [first, last] as any } @@ -111,11 +109,14 @@ export const nodeOps: Omit, 'patchProp'> = { } else { parent.insertAdjacentHTML('beforeend', content) } - return [ - // first - before ? before.nextSibling : parent.firstChild, - // last - anchor ? anchor.previousSibling : parent.lastChild - ] + let first = before ? before.nextSibling : parent.firstChild + const last = anchor ? anchor.previousSibling : parent.lastChild + const ret = [] + while (first) { + ret.push(first) + if (first === last) break + first = first.nextSibling + } + return ret } }