Skip to content

Commit 2701355

Browse files
authored
fix(hydration): avoid hydration mismatch warning for styles with different order (#10011)
close #10000 close #10006
1 parent a3fbf21 commit 2701355

File tree

3 files changed

+92
-11
lines changed

3 files changed

+92
-11
lines changed

packages/runtime-core/__tests__/hydration.spec.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -1431,11 +1431,35 @@ describe('SSR hydration', () => {
14311431
mountWithHydration(`<div style="color:red;"></div>`, () =>
14321432
h('div', { style: `color:red;` }),
14331433
)
1434+
mountWithHydration(
1435+
`<div style="color:red; font-size: 12px;"></div>`,
1436+
() => h('div', { style: `font-size: 12px; color:red;` }),
1437+
)
1438+
mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
1439+
withDirectives(createVNode('div', { style: 'color: red' }, ''), [
1440+
[vShow, false],
1441+
]),
1442+
)
14341443
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
14351444
mountWithHydration(`<div style="color:red;"></div>`, () =>
14361445
h('div', { style: { color: 'green' } }),
14371446
)
1438-
expect(`Hydration style mismatch`).toHaveBeenWarned()
1447+
expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
1448+
})
1449+
1450+
test('style mismatch w/ v-show', () => {
1451+
mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
1452+
withDirectives(createVNode('div', { style: 'color: red' }, ''), [
1453+
[vShow, false],
1454+
]),
1455+
)
1456+
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
1457+
mountWithHydration(`<div style="color:red;"></div>`, () =>
1458+
withDirectives(createVNode('div', { style: 'color: red' }, ''), [
1459+
[vShow, false],
1460+
]),
1461+
)
1462+
expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
14391463
})
14401464

14411465
test('attr mismatch', () => {
@@ -1451,6 +1475,12 @@ describe('SSR hydration', () => {
14511475
mountWithHydration(`<select multiple></div>`, () =>
14521476
h('select', { multiple: 'multiple' }),
14531477
)
1478+
mountWithHydration(`<textarea>foo</textarea>`, () =>
1479+
h('textarea', { value: 'foo' }),
1480+
)
1481+
mountWithHydration(`<textarea></textarea>`, () =>
1482+
h('textarea', { value: '' }),
1483+
)
14541484
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
14551485

14561486
mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))

packages/runtime-core/src/hydration.ts

+56-9
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ export function createHydrationFunctions(
448448
) {
449449
for (const key in props) {
450450
// check hydration mismatch
451-
if (__DEV__ && propHasMismatch(el, key, props[key])) {
451+
if (__DEV__ && propHasMismatch(el, key, props[key], vnode)) {
452452
hasMismatch = true
453453
}
454454
if (
@@ -712,7 +712,12 @@ export function createHydrationFunctions(
712712
/**
713713
* Dev only
714714
*/
715-
function propHasMismatch(el: Element, key: string, clientValue: any): boolean {
715+
function propHasMismatch(
716+
el: Element,
717+
key: string,
718+
clientValue: any,
719+
vnode: VNode,
720+
): boolean {
716721
let mismatchType: string | undefined
717722
let mismatchKey: string | undefined
718723
let actual: any
@@ -726,24 +731,41 @@ function propHasMismatch(el: Element, key: string, clientValue: any): boolean {
726731
mismatchType = mismatchKey = `class`
727732
}
728733
} else if (key === 'style') {
729-
actual = el.getAttribute('style')
730-
expected = isString(clientValue)
731-
? clientValue
732-
: stringifyStyle(normalizeStyle(clientValue))
733-
if (actual !== expected) {
734+
// style might be in different order, but that doesn't affect cascade
735+
actual = toStyleMap(el.getAttribute('style') || '')
736+
expected = toStyleMap(
737+
isString(clientValue)
738+
? clientValue
739+
: stringifyStyle(normalizeStyle(clientValue)),
740+
)
741+
// If `v-show=false`, `display: 'none'` should be added to expected
742+
if (vnode.dirs) {
743+
for (const { dir, value } of vnode.dirs) {
744+
// @ts-expect-error only vShow has this internal name
745+
if (dir.name === 'show' && !value) {
746+
expected.set('display', 'none')
747+
}
748+
}
749+
}
750+
if (!isMapEqual(actual, expected)) {
734751
mismatchType = mismatchKey = 'style'
735752
}
736753
} else if (
737754
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
738755
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
739756
) {
740-
actual = el.hasAttribute(key) && el.getAttribute(key)
757+
// #10000 some attrs such as textarea.value can't be get by `hasAttribute`
758+
actual = el.hasAttribute(key)
759+
? el.getAttribute(key)
760+
: key in el
761+
? el[key as keyof typeof el]
762+
: ''
741763
expected = isBooleanAttr(key)
742764
? includeBooleanAttr(clientValue)
743765
? ''
744766
: false
745767
: clientValue == null
746-
? false
768+
? ''
747769
: String(clientValue)
748770
if (actual !== expected) {
749771
mismatchType = `attribute`
@@ -783,3 +805,28 @@ function isSetEqual(a: Set<string>, b: Set<string>): boolean {
783805
}
784806
return true
785807
}
808+
809+
function toStyleMap(str: string): Map<string, string> {
810+
const styleMap: Map<string, string> = new Map()
811+
for (const item of str.split(';')) {
812+
let [key, value] = item.split(':')
813+
key = key?.trim()
814+
value = value?.trim()
815+
if (key && value) {
816+
styleMap.set(key, value)
817+
}
818+
}
819+
return styleMap
820+
}
821+
822+
function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
823+
if (a.size !== b.size) {
824+
return false
825+
}
826+
for (const [key, value] of a) {
827+
if (value !== b.get(key)) {
828+
return false
829+
}
830+
}
831+
return true
832+
}

packages/runtime-dom/src/directives/vShow.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface VShowElement extends HTMLElement {
77
[vShowOldKey]: string
88
}
99

10-
export const vShow: ObjectDirective<VShowElement> = {
10+
export const vShow: ObjectDirective<VShowElement> & { name?: 'show' } = {
1111
beforeMount(el, { value }, { transition }) {
1212
el[vShowOldKey] = el.style.display === 'none' ? '' : el.style.display
1313
if (transition && value) {
@@ -42,6 +42,10 @@ export const vShow: ObjectDirective<VShowElement> = {
4242
},
4343
}
4444

45+
if (__DEV__) {
46+
vShow.name = 'show'
47+
}
48+
4549
function setDisplay(el: VShowElement, value: unknown): void {
4650
el.style.display = value ? el[vShowOldKey] : 'none'
4751
}

0 commit comments

Comments
 (0)