Skip to content

Commit

Permalink
fix(hydration): avoid hydration mismatch warning for styles with diff…
Browse files Browse the repository at this point in the history
…erent order (vuejs#10011)

close vuejs#10000
close vuejs#10006
  • Loading branch information
zh-lx authored Jan 8, 2024
1 parent a3fbf21 commit 2701355
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 11 deletions.
32 changes: 31 additions & 1 deletion packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,11 +1431,35 @@ describe('SSR hydration', () => {
mountWithHydration(`<div style="color:red;"></div>`, () =>
h('div', { style: `color:red;` }),
)
mountWithHydration(
`<div style="color:red; font-size: 12px;"></div>`,
() => h('div', { style: `font-size: 12px; color:red;` }),
)
mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
withDirectives(createVNode('div', { style: 'color: red' }, ''), [
[vShow, false],
]),
)
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
mountWithHydration(`<div style="color:red;"></div>`, () =>
h('div', { style: { color: 'green' } }),
)
expect(`Hydration style mismatch`).toHaveBeenWarned()
expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
})

test('style mismatch w/ v-show', () => {
mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
withDirectives(createVNode('div', { style: 'color: red' }, ''), [
[vShow, false],
]),
)
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
mountWithHydration(`<div style="color:red;"></div>`, () =>
withDirectives(createVNode('div', { style: 'color: red' }, ''), [
[vShow, false],
]),
)
expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
})

test('attr mismatch', () => {
Expand All @@ -1451,6 +1475,12 @@ describe('SSR hydration', () => {
mountWithHydration(`<select multiple></div>`, () =>
h('select', { multiple: 'multiple' }),
)
mountWithHydration(`<textarea>foo</textarea>`, () =>
h('textarea', { value: 'foo' }),
)
mountWithHydration(`<textarea></textarea>`, () =>
h('textarea', { value: '' }),
)
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()

mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
Expand Down
65 changes: 56 additions & 9 deletions packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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`
Expand Down Expand Up @@ -783,3 +805,28 @@ function isSetEqual(a: Set<string>, b: Set<string>): boolean {
}
return true
}

function toStyleMap(str: string): Map<string, string> {
const styleMap: Map<string, string> = 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<string, string>, b: Map<string, string>): boolean {
if (a.size !== b.size) {
return false
}
for (const [key, value] of a) {
if (value !== b.get(key)) {
return false
}
}
return true
}
6 changes: 5 additions & 1 deletion packages/runtime-dom/src/directives/vShow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface VShowElement extends HTMLElement {
[vShowOldKey]: string
}

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

if (__DEV__) {
vShow.name = 'show'
}

function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el[vShowOldKey] : 'none'
}
Expand Down

0 comments on commit 2701355

Please sign in to comment.