From ddf57ea0be2d9aacc14946a4030f0c979684e666 Mon Sep 17 00:00:00 2001 From: RicardoErii <‘1974364190@qq.com’> Date: Mon, 12 Feb 2024 16:26:39 +0800 Subject: [PATCH] perf(hydration): optimized code --- packages/runtime-core/src/hydration.ts | 228 +++++++++++++------------ 1 file changed, 119 insertions(+), 109 deletions(-) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 1e9200ce27b..32c7b8c0bcf 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -451,7 +451,7 @@ export function createHydrationFunctions( // check hydration mismatch if ( __DEV__ && - propHasMismatch(el, key, props[key], vnode, parentComponent) + propHasMismatch?.(el, key, props[key], vnode, parentComponent) ) { hasMismatch = true } @@ -716,139 +716,149 @@ export function createHydrationFunctions( /** * Dev only */ -function propHasMismatch( +let propHasMismatch: ( el: Element, key: string, clientValue: any, vnode: VNode, instance: ComponentInternalInstance | null, -): boolean { - let mismatchType: string | undefined - let mismatchKey: string | undefined - let actual: any - let expected: any - if (key === 'class') { - // classes might be in different order, but that doesn't affect cascade - // so we just need to check if the class lists contain the same classes. - actual = el.getAttribute('class') - expected = normalizeClass(clientValue) - if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) { - mismatchType = mismatchKey = `class` - } - } else if (key === 'style') { - // style might be in different order, but that doesn't affect cascade - actual = el.getAttribute('style') - expected = isString(clientValue) - ? clientValue - : stringifyStyle(normalizeStyle(clientValue)) - const actualMap = toStyleMap(actual) - const expectedMap = toStyleMap(expected) - // 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) { - expectedMap.set('display', 'none') +) => boolean +if (__DEV__) { + propHasMismatch = function ( + el: Element, + key: string, + clientValue: any, + vnode: VNode, + instance: ComponentInternalInstance | null, + ): boolean { + let mismatchType: string | undefined + let mismatchKey: string | undefined + let actual: any + let expected: any + if (key === 'class') { + // classes might be in different order, but that doesn't affect cascade + // so we just need to check if the class lists contain the same classes. + actual = el.getAttribute('class') + expected = normalizeClass(clientValue) + if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) { + mismatchType = mismatchKey = `class` + } + } else if (key === 'style') { + // style might be in different order, but that doesn't affect cascade + actual = el.getAttribute('style') + expected = isString(clientValue) + ? clientValue + : stringifyStyle(normalizeStyle(clientValue)) + const actualMap = toStyleMap(actual) + const expectedMap = toStyleMap(expected) + // 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) { + expectedMap.set('display', 'none') + } } } - } - const cssVars = instance?.getCssVars?.() - for (const key in cssVars) { - expectedMap.set(`--${key}`, String(cssVars[key])) - } + const cssVars = instance?.getCssVars?.() + for (const key in cssVars) { + expectedMap.set(`--${key}`, String(cssVars[key])) + } - if (!isMapEqual(actualMap, expectedMap)) { - mismatchType = mismatchKey = 'style' - } - } else if ( - (el instanceof SVGElement && isKnownSvgAttr(key)) || - (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) - ) { - if (isBooleanAttr(key)) { - actual = el.hasAttribute(key) - expected = includeBooleanAttr(clientValue) - } else if (clientValue == null) { - actual = el.hasAttribute(key) - expected = false - } else { - if (el.hasAttribute(key)) { - actual = el.getAttribute(key) - } else if (key === 'value' && el.tagName === 'TEXTAREA') { - // #10000 textarea.value can't be retrieved by `hasAttribute` - actual = (el as HTMLTextAreaElement).value + if (!isMapEqual(actualMap, expectedMap)) { + mismatchType = mismatchKey = 'style' + } + } else if ( + (el instanceof SVGElement && isKnownSvgAttr(key)) || + (el instanceof HTMLElement && + (isBooleanAttr(key) || isKnownHtmlAttr(key))) + ) { + if (isBooleanAttr(key)) { + actual = el.hasAttribute(key) + expected = includeBooleanAttr(clientValue) + } else if (clientValue == null) { + actual = el.hasAttribute(key) + expected = false } else { - actual = false + if (el.hasAttribute(key)) { + actual = el.getAttribute(key) + } else if (key === 'value' && el.tagName === 'TEXTAREA') { + // #10000 textarea.value can't be retrieved by `hasAttribute` + actual = (el as HTMLTextAreaElement).value + } else { + actual = false + } + expected = isRenderableAttrValue(clientValue) + ? String(clientValue) + : false + } + if (actual !== expected) { + mismatchType = `attribute` + mismatchKey = key } - expected = isRenderableAttrValue(clientValue) - ? String(clientValue) - : false - } - if (actual !== expected) { - mismatchType = `attribute` - mismatchKey = key } - } - if (mismatchType) { - const format = (v: any) => - v === false ? `(not rendered)` : `${mismatchKey}="${v}"` - const preSegment = `Hydration ${mismatchType} mismatch on` - const postSegment = - `\n - rendered on server: ${format(actual)}` + - `\n - expected on client: ${format(expected)}` + - `\n Note: this mismatch is check-only. The DOM will not be rectified ` + - `in production due to performance overhead.` + - `\n You should fix the source of the mismatch.` - if (__TEST__) { - // during tests, log the full message in one single string for easier - // debugging. - warn(`${preSegment} ${el.tagName}${postSegment}`) - } else { - warn(preSegment, el, postSegment) + if (mismatchType) { + const format = (v: any) => + v === false ? `(not rendered)` : `${mismatchKey}="${v}"` + const preSegment = `Hydration ${mismatchType} mismatch on` + const postSegment = + `\n - rendered on server: ${format(actual)}` + + `\n - expected on client: ${format(expected)}` + + `\n Note: this mismatch is check-only. The DOM will not be rectified ` + + `in production due to performance overhead.` + + `\n You should fix the source of the mismatch.` + if (__TEST__) { + // during tests, log the full message in one single string for easier + // debugging. + warn(`${preSegment} ${el.tagName}${postSegment}`) + } else { + warn(preSegment, el, postSegment) + } + return true } - return true + return false } - return false -} - -function toClassSet(str: string): Set { - return new Set(str.trim().split(/\s+/)) -} -function isSetEqual(a: Set, b: Set): boolean { - if (a.size !== b.size) { - return false + function toClassSet(str: string): Set { + return new Set(str.trim().split(/\s+/)) } - for (const s of a) { - if (!b.has(s)) { + + function isSetEqual(a: Set, b: Set): boolean { + if (a.size !== b.size) { return false } + for (const s of a) { + if (!b.has(s)) { + return false + } + } + return true } - 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) + 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 } - 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)) { + 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 } - return true }