]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hydration): avoid hydration mismatch warning for styles with different order...
authorzhoulixiang <18366276315@163.com>
Mon, 8 Jan 2024 08:36:27 +0000 (16:36 +0800)
committerGitHub <noreply@github.com>
Mon, 8 Jan 2024 08:36:27 +0000 (16:36 +0800)
close #10000
close #10006

packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts
packages/runtime-dom/src/directives/vShow.ts

index 303db51cb4425916543daf09839e1b47e29cac51..0d7df43f6aa1e60d39ae180b1ab365acc843f13d 100644 (file)
@@ -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', () => {
@@ -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' }))
index c2086af8f4eb218d9be0045aa046b167519ab4e7..e3bd42172870dad37595cae30e91a7978ee9831d 100644 (file)
@@ -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<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
+}
index 0e20d7fa140c3f9e7e752efe932835c6c3f1e3a5..2ab25136e74a7ccbd7fa3c2523298f82f1072f84 100644 (file)
@@ -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) {
@@ -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'
 }