fix(runtime-dom): properly handle innerHTML unmount into new children (#11159)

close #9135
This commit is contained in:
linzhe 2024-07-17 16:37:14 +08:00 committed by GitHub
parent b287aeec3e
commit 3e9e32ee0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 40 additions and 92 deletions

View File

@ -465,15 +465,7 @@ export function createHydrationFunctions(
// force hydrate v-bind with .prop modifiers // force hydrate v-bind with .prop modifiers
key[0] === '.' key[0] === '.'
) { ) {
patchProp( patchProp(el, key, null, props[key], undefined, parentComponent)
el,
key,
null,
props[key],
undefined,
undefined,
parentComponent,
)
} }
} }
} else if (props.onClick) { } else if (props.onClick) {
@ -485,7 +477,6 @@ export function createHydrationFunctions(
null, null,
props.onClick, props.onClick,
undefined, undefined,
undefined,
parentComponent, parentComponent,
) )
} else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) { } else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) {

View File

@ -107,10 +107,7 @@ export interface RendererOptions<
prevValue: any, prevValue: any,
nextValue: any, nextValue: any,
namespace?: ElementNamespace, namespace?: ElementNamespace,
prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null, parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
unmountChildren?: UnmountChildrenFn,
): void ): void
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
remove(el: HostNode): void remove(el: HostNode): void
@ -670,17 +667,7 @@ function baseCreateRenderer(
if (props) { if (props) {
for (const key in props) { for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) { if (key !== 'value' && !isReservedProp(key)) {
hostPatchProp( hostPatchProp(el, key, null, props[key], namespace, parentComponent)
el,
key,
null,
props[key],
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren,
)
} }
} }
/** /**
@ -833,6 +820,15 @@ function baseCreateRenderer(
dynamicChildren = null dynamicChildren = null
} }
// #9135 innerHTML / textContent unset needs to happen before possible
// new children mount
if (
(oldProps.innerHTML && newProps.innerHTML == null) ||
(oldProps.textContent && newProps.textContent == null)
) {
hostSetElementText(el, '')
}
if (dynamicChildren) { if (dynamicChildren) {
patchBlockChildren( patchBlockChildren(
n1.dynamicChildren!, n1.dynamicChildren!,
@ -869,15 +865,7 @@ function baseCreateRenderer(
// (i.e. at the exact same position in the source template) // (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) { if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed // element props contain dynamic keys, full diff needed
patchProps( patchProps(el, oldProps, newProps, parentComponent, namespace)
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
namespace,
)
} else { } else {
// class // class
// this flag is matched when the element has dynamic class bindings. // this flag is matched when the element has dynamic class bindings.
@ -908,17 +896,7 @@ function baseCreateRenderer(
const next = newProps[key] const next = newProps[key]
// #1471 force patch value // #1471 force patch value
if (next !== prev || key === 'value') { if (next !== prev || key === 'value') {
hostPatchProp( hostPatchProp(el, key, prev, next, namespace, parentComponent)
el,
key,
prev,
next,
namespace,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren,
)
} }
} }
} }
@ -933,15 +911,7 @@ function baseCreateRenderer(
} }
} else if (!optimized && dynamicChildren == null) { } else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff // unoptimized, full diff
patchProps( patchProps(el, oldProps, newProps, parentComponent, namespace)
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
namespace,
)
} }
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) { if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
@ -998,11 +968,9 @@ function baseCreateRenderer(
const patchProps = ( const patchProps = (
el: RendererElement, el: RendererElement,
vnode: VNode,
oldProps: Data, oldProps: Data,
newProps: Data, newProps: Data,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace, namespace: ElementNamespace,
) => { ) => {
if (oldProps !== newProps) { if (oldProps !== newProps) {
@ -1015,10 +983,7 @@ function baseCreateRenderer(
oldProps[key], oldProps[key],
null, null,
namespace, namespace,
vnode.children as VNode[],
parentComponent, parentComponent,
parentSuspense,
unmountChildren,
) )
} }
} }
@ -1030,17 +995,7 @@ function baseCreateRenderer(
const prev = oldProps[key] const prev = oldProps[key]
// defer patching value // defer patching value
if (next !== prev && key !== 'value') { if (next !== prev && key !== 'value') {
hostPatchProp( hostPatchProp(el, key, prev, next, namespace, parentComponent)
el,
key,
prev,
next,
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren,
)
} }
} }
if ('value' in newProps) { if ('value' in newProps) {

View File

@ -1,5 +1,5 @@
import { patchProp } from '../src/patchProp' import { patchProp } from '../src/patchProp'
import { h, render } from '../src' import { h, nextTick, ref, render } from '../src'
describe('runtime-dom: props patching', () => { describe('runtime-dom: props patching', () => {
test('basic', () => { test('basic', () => {
@ -133,6 +133,25 @@ describe('runtime-dom: props patching', () => {
expect(fn).toHaveBeenCalled() expect(fn).toHaveBeenCalled()
}) })
test('patch innerHTML porp', async () => {
const root = document.createElement('div')
const state = ref(false)
const Comp = {
render: () => {
if (state.value) {
return h('div', [h('del', null, 'baz')])
} else {
return h('div', { innerHTML: 'baz' })
}
},
}
render(h(Comp), root)
expect(root.innerHTML).toBe(`<div>baz</div>`)
state.value = true
await nextTick()
expect(root.innerHTML).toBe(`<div><del>baz</del></div>`)
})
test('textContent unmount prev children', () => { test('textContent unmount prev children', () => {
const fn = vi.fn() const fn = vi.fn()
const comp = { const comp = {

View File

@ -10,19 +10,13 @@ export function patchDOMProp(
el: any, el: any,
key: string, key: string,
value: any, value: any,
// the following args are passed only due to potential innerHTML/textContent
// overriding existing VNodes, in which case the old tree must be properly
// unmounted.
prevChildren: any,
parentComponent: any, parentComponent: any,
parentSuspense: any,
unmountChildren: any,
) { ) {
if (key === 'innerHTML' || key === 'textContent') { if (key === 'innerHTML' || key === 'textContent') {
if (prevChildren) { // null value case is handled in renderer patchElement before patching
unmountChildren(prevChildren, parentComponent, parentSuspense) // children
} if (value === null) return
el[key] = value == null ? '' : value el[key] = value
return return
} }

View File

@ -21,10 +21,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
prevValue, prevValue,
nextValue, nextValue,
namespace, namespace,
prevChildren,
parentComponent, parentComponent,
parentSuspense,
unmountChildren,
) => { ) => {
const isSVG = namespace === 'svg' const isSVG = namespace === 'svg'
if (key === 'class') { if (key === 'class') {
@ -43,15 +40,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
? ((key = key.slice(1)), false) ? ((key = key.slice(1)), false)
: shouldSetAsProp(el, key, nextValue, isSVG) : shouldSetAsProp(el, key, nextValue, isSVG)
) { ) {
patchDOMProp( patchDOMProp(el, key, nextValue, parentComponent)
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren,
)
// #6007 also set form state as attributes so they work with // #6007 also set form state as attributes so they work with
// <input type="reset"> or libs / extensions that expect attributes // <input type="reset"> or libs / extensions that expect attributes
// #11163 custom elements may use value as an prop and set it as object // #11163 custom elements may use value as an prop and set it as object