fix(hydration): force hydration for v-bind with .prop modifier

ref #7490
This commit is contained in:
Evan You 2023-11-10 13:13:07 +08:00
parent 34b5a5da4a
commit 364f319d21
10 changed files with 51 additions and 19 deletions

View File

@ -85,7 +85,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(\\"input\\", { return (_openBlock(), _createElementBlock(\\"input\\", {
\\"foo-value\\": model, \\"foo-value\\": model,
\\"onUpdate:fooValue\\": $event => ((model) = $event) \\"onUpdate:fooValue\\": $event => ((model) = $event)
}, null, 40 /* PROPS, HYDRATE_EVENTS */, [\\"foo-value\\", \\"onUpdate:fooValue\\"])) }, null, 40 /* PROPS, NEED_HYDRATION */, [\\"foo-value\\", \\"onUpdate:fooValue\\"]))
} }
}" }"
`; `;

View File

@ -1089,7 +1089,7 @@ describe('compiler: element transform', () => {
}) })
}) })
test('HYDRATE_EVENTS', () => { test('NEED_HYDRATION for v-on', () => {
// ignore click events (has dedicated fast path) // ignore click events (has dedicated fast path)
const { node } = parseWithElementTransform(`<div @click="foo" />`, { const { node } = parseWithElementTransform(`<div @click="foo" />`, {
directiveTransforms: { directiveTransforms: {
@ -1108,12 +1108,24 @@ describe('compiler: element transform', () => {
} }
) )
expect(node2.patchFlag).toBe( expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
)
})
test('NEED_HYDRATION for v-bind.prop', () => {
const { node } = parseWithBind(`<div v-bind:id.prop="id" />`)
expect(node.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
)
const { node: node2 } = parseWithBind(`<div .id="id" />`)
expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
) )
}) })
// #5870 // #5870
test('HYDRATE_EVENTS on dynamic component', () => { test('NEED_HYDRATION on dynamic component', () => {
const { node } = parseWithElementTransform( const { node } = parseWithElementTransform(
`<component :is="foo" @input="foo" />`, `<component :is="foo" @input="foo" />`,
{ {
@ -1123,7 +1135,7 @@ describe('compiler: element transform', () => {
} }
) )
expect(node.patchFlag).toBe( expect(node.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
) )
}) })
}) })

View File

@ -550,7 +550,7 @@ export function buildProps(
) )
} else { } else {
// directives // directives
const { name, arg, exp, loc } = prop const { name, arg, exp, loc, modifiers } = prop
const isVBind = name === 'bind' const isVBind = name === 'bind'
const isVOn = name === 'on' const isVOn = name === 'on'
@ -678,6 +678,11 @@ export function buildProps(
continue continue
} }
// force hydration for v-bind with .prop modifier
if (isVBind && modifiers.includes('prop')) {
patchFlag |= PatchFlags.NEED_HYDRATION
}
const directiveTransform = context.directiveTransforms[name] const directiveTransform = context.directiveTransforms[name]
if (directiveTransform) { if (directiveTransform) {
// has built-in directive transform. // has built-in directive transform.
@ -743,12 +748,12 @@ export function buildProps(
patchFlag |= PatchFlags.PROPS patchFlag |= PatchFlags.PROPS
} }
if (hasHydrationEventBinding) { if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS patchFlag |= PatchFlags.NEED_HYDRATION
} }
} }
if ( if (
!shouldUseBlock && !shouldUseBlock &&
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) && (patchFlag === 0 || patchFlag === PatchFlags.NEED_HYDRATION) &&
(hasRef || hasVnodeHook || runtimeDirectives.length > 0) (hasRef || hasVnodeHook || runtimeDirectives.length > 0)
) { ) {
patchFlag |= PatchFlags.NEED_PATCH patchFlag |= PatchFlags.NEED_PATCH

View File

@ -272,7 +272,7 @@ describe('compiler-dom: transform v-on', () => {
// should not treat cached handler as dynamicProp, so it should have no // should not treat cached handler as dynamicProp, so it should have no
// dynamicProps flags and only the hydration flag // dynamicProps flags and only the hydration flag
expect((root as any).children[0].codegenNode.patchFlag).toBe( expect((root as any).children[0].codegenNode.patchFlag).toBe(
genFlagText(PatchFlags.HYDRATE_EVENTS) genFlagText(PatchFlags.NEED_HYDRATION)
) )
expect(prop).toMatchObject({ expect(prop).toMatchObject({
key: { key: {

View File

@ -935,6 +935,18 @@ describe('SSR hydration', () => {
) )
}) })
test('force hydrate prop with `.prop` modifier', () => {
const { container } = mountWithHydration(
'<input type="checkbox" :indeterminate.prop="true">',
() =>
h('input', {
type: 'checkbox',
'.indeterminate': true
})
)
expect((container.firstChild! as any).indeterminate).toBe(true)
})
test('force hydrate input v-model with non-string value bindings', () => { test('force hydrate input v-model with non-string value bindings', () => {
const { container } = mountWithHydration( const { container } = mountWithHydration(
'<input type="checkbox" value="true">', '<input type="checkbox" value="true">',

View File

@ -477,13 +477,13 @@ describe('vnode', () => {
expect(vnode.dynamicChildren).toStrictEqual([vnode1]) expect(vnode.dynamicChildren).toStrictEqual([vnode1])
}) })
test('should not track vnodes with only HYDRATE_EVENTS flag', () => { test('should not track vnodes with only NEED_HYDRATION flag', () => {
const hoist = createVNode('div') const hoist = createVNode('div')
const vnode = const vnode =
(openBlock(), (openBlock(),
createBlock('div', null, [ createBlock('div', null, [
hoist, hoist,
createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS) createVNode('div', null, 'text', PatchFlags.NEED_HYDRATION)
])) ]))
expect(vnode.dynamicChildren).toStrictEqual([]) expect(vnode.dynamicChildren).toStrictEqual([])
}) })

View File

@ -334,13 +334,15 @@ export function createHydrationFunctions(
if ( if (
forcePatch || forcePatch ||
!optimized || !optimized ||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.HYDRATE_EVENTS) patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
) { ) {
for (const key in props) { for (const key in props) {
if ( if (
(forcePatch && (forcePatch &&
(key.endsWith('value') || key === 'indeterminate')) || (key.endsWith('value') || key === 'indeterminate')) ||
(isOn(key) && !isReservedProp(key)) (isOn(key) && !isReservedProp(key)) ||
// force hydrate v-bind with .prop modifiers
key[0] === '.'
) { ) {
patchProp( patchProp(
el, el,

View File

@ -2395,7 +2395,7 @@ export function traverseStaticChildren(n1: VNode, n2: VNode, shallow = false) {
const c1 = ch1[i] as VNode const c1 = ch1[i] as VNode
let c2 = ch2[i] as VNode let c2 = ch2[i] as VNode
if (c2.shapeFlag & ShapeFlags.ELEMENT && !c2.dynamicChildren) { if (c2.shapeFlag & ShapeFlags.ELEMENT && !c2.dynamicChildren) {
if (c2.patchFlag <= 0 || c2.patchFlag === PatchFlags.HYDRATE_EVENTS) { if (c2.patchFlag <= 0 || c2.patchFlag === PatchFlags.NEED_HYDRATION) {
c2 = ch2[i] = cloneIfMounted(ch2[i] as VNode) c2 = ch2[i] = cloneIfMounted(ch2[i] as VNode)
c2.el = c1.el c2.el = c1.el
} }

View File

@ -489,7 +489,7 @@ function createBaseVNode(
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) && (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the // the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching. // vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS vnode.patchFlag !== PatchFlags.NEED_HYDRATION
) { ) {
currentBlock.push(vnode) currentBlock.push(vnode)
} }

View File

@ -57,10 +57,11 @@ export const enum PatchFlags {
FULL_PROPS = 1 << 4, FULL_PROPS = 1 << 4,
/** /**
* Indicates an element with event listeners (which need to be attached * Indicates an element that requires props hydration
* during hydration) * (but not necessarily patching)
* e.g. event listeners & v-bind with prop modifier
*/ */
HYDRATE_EVENTS = 1 << 5, NEED_HYDRATION = 1 << 5,
/** /**
* Indicates a fragment whose children order doesn't change. * Indicates a fragment whose children order doesn't change.
@ -131,7 +132,7 @@ export const PatchFlagNames: Record<PatchFlags, string> = {
[PatchFlags.STYLE]: `STYLE`, [PatchFlags.STYLE]: `STYLE`,
[PatchFlags.PROPS]: `PROPS`, [PatchFlags.PROPS]: `PROPS`,
[PatchFlags.FULL_PROPS]: `FULL_PROPS`, [PatchFlags.FULL_PROPS]: `FULL_PROPS`,
[PatchFlags.HYDRATE_EVENTS]: `HYDRATE_EVENTS`, [PatchFlags.NEED_HYDRATION]: `NEED_HYDRATION`,
[PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`, [PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`,
[PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`, [PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`,
[PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`, [PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`,