Skip to content

Commit 66bca69

Browse files
committed
wip: attr mismatch
1 parent e496228 commit 66bca69

File tree

3 files changed

+149
-96
lines changed

3 files changed

+149
-96
lines changed

packages/runtime-core/src/hydration.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -869,29 +869,8 @@ function propHasMismatch(
869869
mismatchType = MismatchTypes.STYLE
870870
mismatchKey = 'style'
871871
}
872-
} else if (
873-
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
874-
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
875-
) {
876-
if (isBooleanAttr(key)) {
877-
actual = el.hasAttribute(key)
878-
expected = includeBooleanAttr(clientValue)
879-
} else if (clientValue == null) {
880-
actual = el.hasAttribute(key)
881-
expected = false
882-
} else {
883-
if (el.hasAttribute(key)) {
884-
actual = el.getAttribute(key)
885-
} else if (key === 'value' && el.tagName === 'TEXTAREA') {
886-
// #10000 textarea.value can't be retrieved by `hasAttribute`
887-
actual = (el as HTMLTextAreaElement).value
888-
} else {
889-
actual = false
890-
}
891-
expected = isRenderableAttrValue(clientValue)
892-
? String(clientValue)
893-
: false
894-
}
872+
} else if (isValidHtmlOrSvgAttribute(el, key)) {
873+
;({ actual, expected } = getAttributeMismatch(el, key, clientValue))
895874
if (actual !== expected) {
896875
mismatchType = MismatchTypes.ATTRIBUTE
897876
mismatchKey = key
@@ -901,6 +880,43 @@ function propHasMismatch(
901880
return warnPropMismatch(el, mismatchKey, mismatchType, actual, expected)
902881
}
903882

883+
export function getAttributeMismatch(
884+
el: Element,
885+
key: string,
886+
clientValue: any,
887+
): {
888+
actual: string | boolean | null | undefined
889+
expected: string | boolean | null | undefined
890+
} {
891+
let actual: string | boolean | null | undefined
892+
let expected: string | boolean | null | undefined
893+
if (isBooleanAttr(key)) {
894+
actual = el.hasAttribute(key)
895+
expected = includeBooleanAttr(clientValue)
896+
} else if (clientValue == null) {
897+
actual = el.hasAttribute(key)
898+
expected = false
899+
} else {
900+
if (el.hasAttribute(key)) {
901+
actual = el.getAttribute(key)
902+
} else if (key === 'value' && el.tagName === 'TEXTAREA') {
903+
// #10000 textarea.value can't be retrieved by `hasAttribute`
904+
actual = (el as HTMLTextAreaElement).value
905+
} else {
906+
actual = false
907+
}
908+
expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false
909+
}
910+
return { actual, expected }
911+
}
912+
913+
export function isValidHtmlOrSvgAttribute(el: Element, key: string): boolean {
914+
return (
915+
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
916+
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
917+
)
918+
}
919+
904920
export function warnPropMismatch(
905921
el: Element & { $cls?: string },
906922
mismatchKey: string | undefined,

packages/runtime-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,4 +573,6 @@ export {
573573
warnPropMismatch,
574574
toStyleMap,
575575
isMapEqual,
576+
isValidHtmlOrSvgAttribute,
577+
getAttributeMismatch,
576578
} from './hydration'

packages/runtime-vapor/src/dom/prop.ts

Lines changed: 108 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { on } from './event'
1313
import {
1414
MismatchTypes,
1515
currentInstance,
16+
getAttributeMismatch,
1617
isMapEqual,
1718
isMismatchAllowed,
1819
isSetEqual,
20+
isValidHtmlOrSvgAttribute,
1921
mergeProps,
2022
patchStyle,
2123
shouldSetAsProp,
@@ -67,6 +69,15 @@ export function setAttr(el: any, key: string, value: any): void {
6769
;(el as any)._falseValue = value
6870
}
6971

72+
if (
73+
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
74+
isHydrating &&
75+
!attributeHasMismatch(el, key, value)
76+
) {
77+
el[`$${key}`] = value
78+
return
79+
}
80+
7081
if (value !== el[`$${key}`]) {
7182
el[`$${key}`] = value
7283
if (value != null) {
@@ -82,6 +93,14 @@ export function setDOMProp(el: any, key: string, value: any): void {
8293
return
8394
}
8495

96+
if (
97+
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
98+
isHydrating &&
99+
!attributeHasMismatch(el, key, value)
100+
) {
101+
return
102+
}
103+
85104
const prev = el[key]
86105
if (value === prev) {
87106
return
@@ -123,32 +142,37 @@ export function setClass(el: TargetElement, value: any): void {
123142
if (el.$root) {
124143
setClassIncremental(el, value)
125144
} else {
145+
value = normalizeClass(value)
126146
if (
127147
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
128-
isHydrating
148+
isHydrating &&
149+
!classHasMismatch(el, value, false)
129150
) {
130-
const expected = normalizeClass(value)
131-
handleClassHydration(el, value, expected, false, '$cls')
151+
el.$cls = value
132152
return
133153
}
134154

135-
if ((value = normalizeClass(value)) !== el.$cls) {
155+
if (value !== el.$cls) {
136156
el.className = el.$cls = value
137157
}
138158
}
139159
}
140160

141161
function setClassIncremental(el: any, value: any): void {
142162
const cacheKey = `$clsi${isApplyingFallthroughProps ? '$' : ''}`
163+
const normalizedValue = normalizeClass(value)
143164

144-
if ((__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && isHydrating) {
145-
const expected = normalizeClass(value)
146-
handleClassHydration(el, value, expected, true, cacheKey)
165+
if (
166+
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
167+
isHydrating &&
168+
!classHasMismatch(el, normalizedValue, true)
169+
) {
170+
el[cacheKey] = normalizedValue
147171
return
148172
}
149173

150174
const prev = el[cacheKey]
151-
if ((value = el[cacheKey] = normalizeClass(value)) !== prev) {
175+
if ((value = el[cacheKey] = normalizedValue) !== prev) {
152176
const nextList = value.split(/\s+/)
153177
if (value) {
154178
el.classList.add(...nextList)
@@ -165,16 +189,17 @@ export function setStyle(el: TargetElement, value: any): void {
165189
if (el.$root) {
166190
setStyleIncremental(el, value)
167191
} else {
192+
const normalizedValue = normalizeStyle(value)
168193
if (
169194
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
170-
isHydrating
195+
isHydrating &&
196+
!styleHasMismatch(el, value, normalizedValue, false)
171197
) {
172-
const normalizedValue = normalizeStyle(value)
173-
handleStyleHydration(el, value, normalizedValue, false, '$sty')
198+
el.$sty = normalizedValue
174199
return
175200
}
176201

177-
patchStyle(el, el.$sty, (el.$sty = normalizeStyle(value)))
202+
patchStyle(el, el.$sty, (el.$sty = normalizedValue))
178203
}
179204
}
180205

@@ -184,8 +209,12 @@ function setStyleIncremental(el: any, value: any): NormalizedStyle | undefined {
184209
? parseStringStyle(value)
185210
: (normalizeStyle(value) as NormalizedStyle | undefined)
186211

187-
if ((__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && isHydrating) {
188-
handleStyleHydration(el, value, normalizedValue, true, cacheKey)
212+
if (
213+
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
214+
isHydrating &&
215+
!styleHasMismatch(el, value, normalizedValue, true)
216+
) {
217+
el[cacheKey] = normalizedValue
189218
return
190219
}
191220

@@ -197,6 +226,14 @@ export function setValue(el: TargetElement, value: any): void {
197226
return
198227
}
199228

229+
if (
230+
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
231+
isHydrating &&
232+
!attributeHasMismatch(el, 'value', value)
233+
) {
234+
return
235+
}
236+
200237
// store value as _value as well since
201238
// non-string values will be stringified.
202239
el._value = value
@@ -219,20 +256,19 @@ export function setValue(el: TargetElement, value: any): void {
219256
*/
220257
export function setText(el: Text & { $txt?: string }, value: string): void {
221258
if (isHydrating) {
222-
if (el.nodeValue !== value) {
223-
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
224-
warn(
225-
`Hydration text mismatch in`,
226-
el.parentNode,
227-
`\n - rendered on server: ${JSON.stringify((el as Text).data)}` +
228-
`\n - expected on client: ${JSON.stringify(value)}`,
229-
)
230-
logMismatchError()
231-
el.nodeValue = value
259+
if (el.nodeValue == value) {
260+
el.$txt = value
261+
return
232262
}
233263

234-
el.$txt = value
235-
return
264+
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
265+
warn(
266+
`Hydration text mismatch in`,
267+
el.parentNode,
268+
`\n - rendered on server: ${JSON.stringify((el as Text).data)}` +
269+
`\n - expected on client: ${JSON.stringify(value)}`,
270+
)
271+
logMismatchError()
236272
}
237273

238274
if (el.$txt !== value) {
@@ -258,22 +294,21 @@ export function setElementText(
258294
clientText = clientText.slice(1)
259295
}
260296

261-
if (el.textContent !== clientText) {
262-
if (!isMismatchAllowed(el as Element, MismatchTypes.TEXT)) {
263-
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
264-
warn(
265-
`Hydration text content mismatch on`,
266-
el,
267-
`\n - rendered on server: ${el.textContent}` +
268-
`\n - expected on client: ${clientText}`,
269-
)
270-
logMismatchError()
271-
}
272-
el.textContent = clientText
297+
if (el.textContent === clientText) {
298+
el.$txt = clientText
299+
return
273300
}
274301

275-
el.$txt = clientText
276-
return
302+
if (!isMismatchAllowed(el as Element, MismatchTypes.TEXT)) {
303+
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
304+
warn(
305+
`Hydration text content mismatch on`,
306+
el,
307+
`\n - rendered on server: ${el.textContent}` +
308+
`\n - expected on client: ${clientText}`,
309+
)
310+
logMismatchError()
311+
}
277312
}
278313

279314
if (el.$txt !== value) {
@@ -285,22 +320,21 @@ export function setHtml(el: TargetElement, value: any): void {
285320
value = value == null ? '' : value
286321

287322
if (isHydrating) {
288-
if (el.innerHTML !== value) {
289-
if (!isMismatchAllowed(el, MismatchTypes.CHILDREN)) {
290-
if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
291-
warn(
292-
`Hydration children mismatch on`,
293-
el,
294-
`\nServer rendered element contains different child nodes from client nodes.`,
295-
)
296-
}
297-
logMismatchError()
298-
}
299-
el.innerHTML = value
323+
if (el.innerHTML === value) {
324+
el.$html = value
325+
return
300326
}
301327

302-
el.$html = value
303-
return
328+
if (!isMismatchAllowed(el, MismatchTypes.CHILDREN)) {
329+
if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
330+
warn(
331+
`Hydration children mismatch on`,
332+
el,
333+
`\nServer rendered element contains different child nodes from client nodes.`,
334+
)
335+
}
336+
logMismatchError()
337+
}
304338
}
305339

306340
if (el.$html !== value) {
@@ -384,13 +418,11 @@ export function optimizePropertyLookup(): void {
384418
''
385419
}
386420

387-
function handleClassHydration(
421+
function classHasMismatch(
388422
el: TargetElement | any,
389-
value: any,
390423
expected: string,
391424
isIncremental: boolean,
392-
cacheKey: string,
393-
) {
425+
): boolean {
394426
const actual = el.getAttribute('class')
395427
const actualClassSet = toClassSet(actual || '')
396428
const expectedClassSet = toClassSet(expected)
@@ -403,27 +435,18 @@ function handleClassHydration(
403435
if (hasMismatch) {
404436
warnPropMismatch(el, 'class', MismatchTypes.CLASS, actual, expected)
405437
logMismatchError()
406-
407-
if (isIncremental) {
408-
const nextList = value.split(/\s+/)
409-
if (value) {
410-
el.classList.add(...nextList)
411-
}
412-
} else {
413-
el.className = expected
414-
}
438+
return true
415439
}
416440

417-
el[cacheKey] = expected
441+
return false
418442
}
419443

420-
function handleStyleHydration(
444+
function styleHasMismatch(
421445
el: TargetElement | any,
422446
value: any,
423447
normalizedValue: string | NormalizedStyle | undefined,
424448
isIncremental: boolean,
425-
cacheKey: string,
426-
) {
449+
): boolean {
427450
const actual = el.getAttribute('style')
428451
const actualStyleMap = toStyleMap(actual || '')
429452
const expected = isString(value) ? value : stringifyStyle(normalizedValue)
@@ -446,8 +469,20 @@ function handleStyleHydration(
446469
if (hasMismatch) {
447470
warnPropMismatch(el, 'style', MismatchTypes.STYLE, actual, expected)
448471
logMismatchError()
449-
patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
472+
return true
450473
}
451474

452-
el[cacheKey] = normalizedValue
475+
return false
476+
}
477+
478+
function attributeHasMismatch(el: any, key: string, value: any): boolean {
479+
if (isValidHtmlOrSvgAttribute(el, key)) {
480+
const { actual, expected } = getAttributeMismatch(el, key, value)
481+
if (actual !== expected) {
482+
warnPropMismatch(el, key, MismatchTypes.ATTRIBUTE, actual, expected)
483+
logMismatchError()
484+
return true
485+
}
486+
}
487+
return false
453488
}

0 commit comments

Comments
 (0)