From 2701355e8eb07ab664e398d9fc05d6c4e2e9b20e Mon Sep 17 00:00:00 2001 From: zhoulixiang <18366276315@163.com> Date: Mon, 8 Jan 2024 16:36:27 +0800 Subject: [PATCH] fix(hydration): avoid hydration mismatch warning for styles with different order (#10011) close #10000 close #10006 --- .../runtime-core/__tests__/hydration.spec.ts | 32 ++++++++- packages/runtime-core/src/hydration.ts | 65 ++++++++++++++++--- packages/runtime-dom/src/directives/vShow.ts | 6 +- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 303db51cb44..0d7df43f6aa 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -1431,11 +1431,35 @@ describe('SSR hydration', () => { mountWithHydration(`
`, () => h('div', { style: `color:red;` }), ) + mountWithHydration( + `
`, + () => h('div', { style: `font-size: 12px; color:red;` }), + ) + mountWithHydration(`
`, () => + withDirectives(createVNode('div', { style: 'color: red' }, ''), [ + [vShow, false], + ]), + ) expect(`Hydration style mismatch`).not.toHaveBeenWarned() mountWithHydration(`
`, () => h('div', { style: { color: 'green' } }), ) - expect(`Hydration style mismatch`).toHaveBeenWarned() + expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1) + }) + + test('style mismatch w/ v-show', () => { + mountWithHydration(`
`, () => + withDirectives(createVNode('div', { style: 'color: red' }, ''), [ + [vShow, false], + ]), + ) + expect(`Hydration style mismatch`).not.toHaveBeenWarned() + mountWithHydration(`
`, () => + withDirectives(createVNode('div', { style: 'color: red' }, ''), [ + [vShow, false], + ]), + ) + expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1) }) test('attr mismatch', () => { @@ -1451,6 +1475,12 @@ describe('SSR hydration', () => { mountWithHydration(``, () => + h('textarea', { value: 'foo' }), + ) + mountWithHydration(``, () => + h('textarea', { value: '' }), + ) expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() mountWithHydration(`
`, () => h('div', { id: 'foo' })) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index c2086af8f4e..e3bd4217287 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -448,7 +448,7 @@ export function createHydrationFunctions( ) { for (const key in props) { // check hydration mismatch - if (__DEV__ && propHasMismatch(el, key, props[key])) { + if (__DEV__ && propHasMismatch(el, key, props[key], vnode)) { hasMismatch = true } if ( @@ -712,7 +712,12 @@ export function createHydrationFunctions( /** * Dev only */ -function propHasMismatch(el: Element, key: string, clientValue: any): boolean { +function propHasMismatch( + el: Element, + key: string, + clientValue: any, + vnode: VNode, +): boolean { let mismatchType: string | undefined let mismatchKey: string | undefined let actual: any @@ -726,24 +731,41 @@ function propHasMismatch(el: Element, key: string, clientValue: any): boolean { mismatchType = mismatchKey = `class` } } else if (key === 'style') { - actual = el.getAttribute('style') - expected = isString(clientValue) - ? clientValue - : stringifyStyle(normalizeStyle(clientValue)) - if (actual !== expected) { + // style might be in different order, but that doesn't affect cascade + actual = toStyleMap(el.getAttribute('style') || '') + expected = toStyleMap( + isString(clientValue) + ? clientValue + : stringifyStyle(normalizeStyle(clientValue)), + ) + // If `v-show=false`, `display: 'none'` should be added to expected + if (vnode.dirs) { + for (const { dir, value } of vnode.dirs) { + // @ts-expect-error only vShow has this internal name + if (dir.name === 'show' && !value) { + expected.set('display', 'none') + } + } + } + if (!isMapEqual(actual, expected)) { mismatchType = mismatchKey = 'style' } } else if ( (el instanceof SVGElement && isKnownSvgAttr(key)) || (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) ) { - actual = el.hasAttribute(key) && el.getAttribute(key) + // #10000 some attrs such as textarea.value can't be get by `hasAttribute` + actual = el.hasAttribute(key) + ? el.getAttribute(key) + : key in el + ? el[key as keyof typeof el] + : '' expected = isBooleanAttr(key) ? includeBooleanAttr(clientValue) ? '' : false : clientValue == null - ? false + ? '' : String(clientValue) if (actual !== expected) { mismatchType = `attribute` @@ -783,3 +805,28 @@ function isSetEqual(a: Set, b: Set): boolean { } return true } + +function toStyleMap(str: string): Map { + const styleMap: Map = new Map() + for (const item of str.split(';')) { + let [key, value] = item.split(':') + key = key?.trim() + value = value?.trim() + if (key && value) { + styleMap.set(key, value) + } + } + return styleMap +} + +function isMapEqual(a: Map, b: Map): boolean { + if (a.size !== b.size) { + return false + } + for (const [key, value] of a) { + if (value !== b.get(key)) { + return false + } + } + return true +} diff --git a/packages/runtime-dom/src/directives/vShow.ts b/packages/runtime-dom/src/directives/vShow.ts index 0e20d7fa140..2ab25136e74 100644 --- a/packages/runtime-dom/src/directives/vShow.ts +++ b/packages/runtime-dom/src/directives/vShow.ts @@ -7,7 +7,7 @@ interface VShowElement extends HTMLElement { [vShowOldKey]: string } -export const vShow: ObjectDirective = { +export const vShow: ObjectDirective & { name?: 'show' } = { beforeMount(el, { value }, { transition }) { el[vShowOldKey] = el.style.display === 'none' ? '' : el.style.display if (transition && value) { @@ -42,6 +42,10 @@ export const vShow: ObjectDirective = { }, } +if (__DEV__) { + vShow.name = 'show' +} + function setDisplay(el: VShowElement, value: unknown): void { el.style.display = value ? el[vShowOldKey] : 'none' }