diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index c7549ae1598..66319fce3b8 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -141,7 +141,8 @@ export interface RendererOptions< content: string, parent: HostElement, anchor: HostNode | null, - isSVG: boolean + isSVG: boolean, + cached?: [HostNode, HostNode | null] | null ): HostElement[] } @@ -635,7 +636,11 @@ function baseCreateRenderer( n2.children as string, container, anchor, - isSVG + isSVG, + // 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] ) } diff --git a/packages/runtime-dom/__tests__/nodeOps.spec.ts b/packages/runtime-dom/__tests__/nodeOps.spec.ts index ce9764471d1..5e625ff53c4 100644 --- a/packages/runtime-dom/__tests__/nodeOps.spec.ts +++ b/packages/runtime-dom/__tests__/nodeOps.spec.ts @@ -1,4 +1,4 @@ -import { nodeOps } from '../src/nodeOps' +import { nodeOps, svgNS } from '../src/nodeOps' describe('runtime-dom: node-ops', () => { test('the _value property should be cloned', () => { @@ -25,4 +25,101 @@ describe('runtime-dom: node-ops', () => { expect(option1.selected).toBe(true) expect(option2.selected).toBe(true) }) + + describe('insertStaticContent', () => { + test('fresh insertion', () => { + const content = `
one
two
three` + const parent = document.createElement('div') + const [first, last] = nodeOps.insertStaticContent!( + content, + parent, + null, + false + ) + expect(parent.innerHTML).toBe(content) + expect(first).toBe(parent.firstChild) + expect(last).toBe(parent.lastChild) + }) + + test('fresh insertion with anchor', () => { + const content = `
one
two
three` + const existing = `
existing
` + const parent = document.createElement('div') + parent.innerHTML = existing + const anchor = parent.firstChild + const [first, last] = 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]) + }) + + test('fresh insertion as svg', () => { + const content = `hello` + const parent = document.createElementNS(svgNS, 'svg') + const [first, last] = nodeOps.insertStaticContent!( + content, + parent, + null, + true + ) + expect(parent.innerHTML).toBe(content) + expect(first).toBe(parent.firstChild) + expect(last).toBe(parent.lastChild) + expect(first.namespaceURI).toMatch('svg') + expect(last.namespaceURI).toMatch('svg') + }) + + test('fresh insertion as svg, with anchor', () => { + const content = `hello` + const existing = `` + const parent = document.createElementNS(svgNS, 'svg') + parent.innerHTML = existing + const anchor = parent.firstChild + const [first, last] = nodeOps.insertStaticContent!( + content, + parent, + anchor, + true + ) + expect(parent.innerHTML).toBe(content + existing) + expect(first).toBe(parent.firstChild) + expect(last).toBe(parent.childNodes[parent.childNodes.length - 2]) + expect(first.namespaceURI).toMatch('svg') + expect(last.namespaceURI).toMatch('svg') + }) + + test('cached', () => { + const content = `
one
two
three` + + const cacheParent = document.createElement('div') + const [cachedFirst, cachedLast] = nodeOps.insertStaticContent!( + content, + cacheParent, + null, + false + ) + + const parent = document.createElement('div') + + const [first, last] = nodeOps.insertStaticContent!( + ``, + parent, + null, + false, + [cachedFirst, cachedLast] + ) + + 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) + }) + }) }) diff --git a/packages/runtime-dom/src/nodeOps.ts b/packages/runtime-dom/src/nodeOps.ts index 95d9940a5fd..087725aae3a 100644 --- a/packages/runtime-dom/src/nodeOps.ts +++ b/packages/runtime-dom/src/nodeOps.ts @@ -4,9 +4,6 @@ export const svgNS = 'http://www.w3.org/2000/svg' const doc = (typeof document !== 'undefined' ? document : null) as Document -let tempContainer: HTMLElement -let tempSVGContainer: SVGElement - export const nodeOps: Omit, 'patchProp'> = { insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null) @@ -71,23 +68,54 @@ export const nodeOps: Omit, 'patchProp'> = { }, // __UNSAFE__ - // Reason: innerHTML. + // Reason: insertAdjacentHTML. // Static content here can only come from compiled templates. // As long as the user only uses trusted templates, this is safe. - insertStaticContent(content, parent, anchor, isSVG) { - const temp = isSVG - ? tempSVGContainer || - (tempSVGContainer = doc.createElementNS(svgNS, 'svg')) - : tempContainer || (tempContainer = doc.createElement('div')) - temp.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 + 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 + parent.insertBefore(node, anchor) + if (cachedFirst === cachedLast) { + last = node + break + } + cachedFirst = cachedFirst.nextSibling! + } + return [first, last] as any + } + + // before | first ... last | anchor + const before = anchor ? anchor.previousSibling : parent.lastChild + if (anchor) { + let insertionPoint + let usingTempInsertionPoint = false + if (anchor instanceof Element) { + insertionPoint = anchor + } else { + // insertAdjacentHTML only works for elements but the anchor is not an + // element... + usingTempInsertionPoint = true + insertionPoint = isSVG + ? doc.createElementNS(svgNS, 'g') + : doc.createElement('div') + parent.insertBefore(insertionPoint, anchor) + } + insertionPoint.insertAdjacentHTML('beforebegin', content) + if (usingTempInsertionPoint) { + parent.removeChild(insertionPoint) + } + } else { + parent.insertAdjacentHTML('beforeend', content) } - return [first, last] + return [ + // first + before ? before.nextSibling : parent.firstChild, + // last + anchor ? anchor.previousSibling : parent.lastChild + ] } }