Skip to content

Commit 3bc2914

Browse files
committed
fix(attr-fallthrough): ensure consistent attr fallthrough for root fragments with comments
fix #2549
1 parent 3532b2b commit 3bc2914

File tree

5 files changed

+172
-90
lines changed

5 files changed

+172
-90
lines changed

packages/compiler-core/__tests__/transform.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,24 @@ describe('compiler: transform', () => {
342342
)
343343
)
344344
})
345+
346+
test('multiple children w/ single root + comments', () => {
347+
const ast = transformWithCodegen(`<!--foo--><div/><!--bar-->`)
348+
expect(ast.codegenNode).toMatchObject(
349+
createBlockMatcher(
350+
FRAGMENT,
351+
undefined,
352+
[
353+
{ type: NodeTypes.COMMENT },
354+
{ type: NodeTypes.ELEMENT, tag: `div` },
355+
{ type: NodeTypes.COMMENT }
356+
] as any,
357+
genFlagText([
358+
PatchFlags.STABLE_FRAGMENT,
359+
PatchFlags.DEV_ROOT_FRAGMENT
360+
])
361+
)
362+
)
363+
})
345364
})
346365
})

packages/compiler-core/src/transform.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -314,14 +314,23 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
314314
}
315315
} else if (children.length > 1) {
316316
// root has multiple nodes - return a fragment block.
317+
let patchFlag = PatchFlags.STABLE_FRAGMENT
318+
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
319+
// check if the fragment actually contains a single valid child with
320+
// the rest being comments
321+
if (
322+
__DEV__ &&
323+
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
324+
) {
325+
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
326+
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
327+
}
317328
root.codegenNode = createVNodeCall(
318329
context,
319330
helper(FRAGMENT),
320331
undefined,
321332
root.children,
322-
`${PatchFlags.STABLE_FRAGMENT} /* ${
323-
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
324-
} */`,
333+
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
325334
undefined,
326335
undefined,
327336
true

packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
createCommentVNode,
1414
Fragment
1515
} from '@vue/runtime-dom'
16+
import { PatchFlags } from '@vue/shared/src'
1617

1718
describe('attribute fallthrough', () => {
1819
it('should allow attrs to fallthrough', async () => {
@@ -574,11 +575,16 @@ describe('attribute fallthrough', () => {
574575
setup() {
575576
return () => (
576577
openBlock(),
577-
createBlock(Fragment, null, [
578-
createCommentVNode('hello'),
579-
h('button'),
580-
createCommentVNode('world')
581-
])
578+
createBlock(
579+
Fragment,
580+
null,
581+
[
582+
createCommentVNode('hello'),
583+
h('button'),
584+
createCommentVNode('world')
585+
],
586+
PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT
587+
)
582588
)
583589
}
584590
}

packages/runtime-core/src/componentRenderUtils.ts

+31-22
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
createVNode,
1010
Comment,
1111
cloneVNode,
12-
Fragment,
1312
VNodeArrayChildren,
1413
isVNode
1514
} from './vnode'
@@ -20,8 +19,10 @@ import { isHmrUpdating } from './hmr'
2019
import { NormalizedProps } from './componentProps'
2120
import { isEmitListener } from './componentEmits'
2221

23-
// mark the current rendering instance for asset resolution (e.g.
24-
// resolveComponent, resolveDirective) during render
22+
/**
23+
* mark the current rendering instance for asset resolution (e.g.
24+
* resolveComponent, resolveDirective) during render
25+
*/
2526
export let currentRenderingInstance: ComponentInternalInstance | null = null
2627

2728
export function setCurrentRenderingInstance(
@@ -30,9 +31,11 @@ export function setCurrentRenderingInstance(
3031
currentRenderingInstance = instance
3132
}
3233

33-
// dev only flag to track whether $attrs was used during render.
34-
// If $attrs was used during render then the warning for failed attrs
35-
// fallthrough can be suppressed.
34+
/**
35+
* dev only flag to track whether $attrs was used during render.
36+
* If $attrs was used during render then the warning for failed attrs
37+
* fallthrough can be suppressed.
38+
*/
3639
let accessedAttrs: boolean = false
3740

3841
export function markAttrsAccessed() {
@@ -116,7 +119,7 @@ export function renderComponentRoot(
116119
// to have comments along side the root element which makes it a fragment
117120
let root = result
118121
let setRoot: ((root: VNode) => void) | undefined = undefined
119-
if (__DEV__) {
122+
if (__DEV__ && result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) {
120123
;[root, setRoot] = getChildRoot(result)
121124
}
122125

@@ -222,9 +225,6 @@ export function renderComponentRoot(
222225
const getChildRoot = (
223226
vnode: VNode
224227
): [VNode, ((root: VNode) => void) | undefined] => {
225-
if (vnode.type !== Fragment) {
226-
return [vnode, undefined]
227-
}
228228
const rawChildren = vnode.children as VNodeArrayChildren
229229
const dynamicChildren = vnode.dynamicChildren
230230
const childRoot = filterSingleRoot(rawChildren)
@@ -246,18 +246,27 @@ const getChildRoot = (
246246
return [normalizeVNode(childRoot), setRoot]
247247
}
248248

249-
/**
250-
* dev only
251-
*/
252-
export function filterSingleRoot(children: VNodeArrayChildren): VNode | null {
253-
const filtered = children.filter(child => {
254-
return !(
255-
isVNode(child) &&
256-
child.type === Comment &&
257-
child.children !== 'v-if'
258-
)
259-
})
260-
return filtered.length === 1 && isVNode(filtered[0]) ? filtered[0] : null
249+
export function filterSingleRoot(
250+
children: VNodeArrayChildren
251+
): VNode | undefined {
252+
let singleRoot
253+
for (let i = 0; i < children.length; i++) {
254+
const child = children[i]
255+
if (isVNode(child)) {
256+
// ignore user comment
257+
if (child.type !== Comment || child.children === 'v-if') {
258+
if (singleRoot) {
259+
// has more than 1 non-comment child, return now
260+
return
261+
} else {
262+
singleRoot = child
263+
}
264+
}
265+
} else {
266+
return
267+
}
268+
}
269+
return singleRoot
261270
}
262271

263272
const getFunctionalFallthrough = (attrs: Data): Data | undefined => {

packages/shared/src/patchFlags.ts

+99-60
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,127 @@
1-
// Patch flags are optimization hints generated by the compiler.
2-
// when a block with dynamicChildren is encountered during diff, the algorithm
3-
// enters "optimized mode". In this mode, we know that the vdom is produced by
4-
// a render function generated by the compiler, so the algorithm only needs to
5-
// handle updates explicitly marked by these patch flags.
6-
7-
// Patch flags can be combined using the | bitwise operator and can be checked
8-
// using the & operator, e.g.
9-
//
10-
// const flag = TEXT | CLASS
11-
// if (flag & TEXT) { ... }
12-
//
13-
// Check the `patchElement` function in './renderer.ts' to see how the
14-
// flags are handled during diff.
15-
1+
/**
2+
* Patch flags are optimization hints generated by the compiler.
3+
* when a block with dynamicChildren is encountered during diff, the algorithm
4+
* enters "optimized mode". In this mode, we know that the vdom is produced by
5+
* a render function generated by the compiler, so the algorithm only needs to
6+
* handle updates explicitly marked by these patch flags.
7+
*
8+
* Patch flags can be combined using the | bitwise operator and can be checked
9+
* using the & operator, e.g.
10+
*
11+
* ```js
12+
* const flag = TEXT | CLASS
13+
* if (flag & TEXT) { ... }
14+
* ```
15+
*
16+
* Check the `patchElement` function in './renderer.ts' to see how the
17+
* flags are handled during diff.
18+
*/
1619
export const enum PatchFlags {
17-
// Indicates an element with dynamic textContent (children fast path)
20+
/**
21+
* Indicates an element with dynamic textContent (children fast path)
22+
*/
1823
TEXT = 1,
1924

20-
// Indicates an element with dynamic class binding.
25+
/**
26+
* Indicates an element with dynamic class binding.
27+
*/
2128
CLASS = 1 << 1,
2229

23-
// Indicates an element with dynamic style
24-
// The compiler pre-compiles static string styles into static objects
25-
// + detects and hoists inline static objects
26-
// e.g. style="color: red" and :style="{ color: 'red' }" both get hoisted as
27-
// const style = { color: 'red' }
28-
// render() { return e('div', { style }) }
30+
/**
31+
* Indicates an element with dynamic style
32+
* The compiler pre-compiles static string styles into static objects
33+
* + detects and hoists inline static objects
34+
* e.g. style="color: red" and :style="{ color: 'red' }" both get hoisted as
35+
* const style = { color: 'red' }
36+
* render() { return e('div', { style }) }
37+
*/
2938
STYLE = 1 << 2,
3039

31-
// Indicates an element that has non-class/style dynamic props.
32-
// Can also be on a component that has any dynamic props (includes
33-
// class/style). when this flag is present, the vnode also has a dynamicProps
34-
// array that contains the keys of the props that may change so the runtime
35-
// can diff them faster (without having to worry about removed props)
40+
/**
41+
* Indicates an element that has non-class/style dynamic props.
42+
* Can also be on a component that has any dynamic props (includes
43+
* class/style). when this flag is present, the vnode also has a dynamicProps
44+
* array that contains the keys of the props that may change so the runtime
45+
* can diff them faster (without having to worry about removed props)
46+
*/
3647
PROPS = 1 << 3,
3748

38-
// Indicates an element with props with dynamic keys. When keys change, a full
39-
// diff is always needed to remove the old key. This flag is mutually
40-
// exclusive with CLASS, STYLE and PROPS.
49+
/**
50+
* Indicates an element with props with dynamic keys. When keys change, a full
51+
* diff is always needed to remove the old key. This flag is mutually
52+
* exclusive with CLASS, STYLE and PROPS.
53+
*/
4154
FULL_PROPS = 1 << 4,
4255

43-
// Indicates an element with event listeners (which need to be attached
44-
// during hydration)
56+
/**
57+
* Indicates an element with event listeners (which need to be attached
58+
* during hydration)
59+
*/
4560
HYDRATE_EVENTS = 1 << 5,
4661

47-
// Indicates a fragment whose children order doesn't change.
62+
/**
63+
* Indicates a fragment whose children order doesn't change.
64+
*/
4865
STABLE_FRAGMENT = 1 << 6,
4966

50-
// Indicates a fragment with keyed or partially keyed children
67+
/**
68+
* Indicates a fragment with keyed or partially keyed children
69+
*/
5170
KEYED_FRAGMENT = 1 << 7,
5271

53-
// Indicates a fragment with unkeyed children.
72+
/**
73+
* Indicates a fragment with unkeyed children.
74+
*/
5475
UNKEYED_FRAGMENT = 1 << 8,
5576

56-
// Indicates an element that only needs non-props patching, e.g. ref or
57-
// directives (onVnodeXXX hooks). since every patched vnode checks for refs
58-
// and onVnodeXXX hooks, it simply marks the vnode so that a parent block
59-
// will track it.
77+
/**
78+
* Indicates an element that only needs non-props patching, e.g. ref or
79+
* directives (onVnodeXXX hooks). since every patched vnode checks for refs
80+
* and onVnodeXXX hooks, it simply marks the vnode so that a parent block
81+
* will track it.
82+
*/
6083
NEED_PATCH = 1 << 9,
6184

62-
// Indicates a component with dynamic slots (e.g. slot that references a v-for
63-
// iterated value, or dynamic slot names).
64-
// Components with this flag are always force updated.
85+
/**
86+
* Indicates a component with dynamic slots (e.g. slot that references a v-for
87+
* iterated value, or dynamic slot names).
88+
* Components with this flag are always force updated.
89+
*/
6590
DYNAMIC_SLOTS = 1 << 10,
6691

67-
// SPECIAL FLAGS -------------------------------------------------------------
68-
69-
// Special flags are negative integers. They are never matched against using
70-
// bitwise operators (bitwise matching should only happen in branches where
71-
// patchFlag > 0), and are mutually exclusive. When checking for a special
72-
// flag, simply check patchFlag === FLAG.
73-
74-
// Indicates a hoisted static vnode. This is a hint for hydration to skip
75-
// the entire sub tree since static content never needs to be updated.
92+
/**
93+
* Indicates a fragment that was created only because the user has placed
94+
* comments at the root level of a template. This is a dev-only flag since
95+
* comments are stripped in production.
96+
*/
97+
DEV_ROOT_FRAGMENT = 1 << 11,
98+
99+
/**
100+
* SPECIAL FLAGS -------------------------------------------------------------
101+
* Special flags are negative integers. They are never matched against using
102+
* bitwise operators (bitwise matching should only happen in branches where
103+
* patchFlag > 0), and are mutually exclusive. When checking for a special
104+
* flag, simply check patchFlag === FLAG.
105+
*/
106+
107+
/**
108+
* Indicates a hoisted static vnode. This is a hint for hydration to skip
109+
* the entire sub tree since static content never needs to be updated.
110+
*/
76111
HOISTED = -1,
77-
78-
// A special flag that indicates that the diffing algorithm should bail out
79-
// of optimized mode. For example, on block fragments created by renderSlot()
80-
// when encountering non-compiler generated slots (i.e. manually written
81-
// render functions, which should always be fully diffed)
82-
// OR manually cloneVNodes
112+
/**
113+
* A special flag that indicates that the diffing algorithm should bail out
114+
* of optimized mode. For example, on block fragments created by renderSlot()
115+
* when encountering non-compiler generated slots (i.e. manually written
116+
* render functions, which should always be fully diffed)
117+
* OR manually cloneVNodes
118+
*/
83119
BAIL = -2
84120
}
85121

86-
// dev only flag -> name mapping
122+
/**
123+
* dev only flag -> name mapping
124+
*/
87125
export const PatchFlagNames = {
88126
[PatchFlags.TEXT]: `TEXT`,
89127
[PatchFlags.CLASS]: `CLASS`,
@@ -94,8 +132,9 @@ export const PatchFlagNames = {
94132
[PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`,
95133
[PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`,
96134
[PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`,
97-
[PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`,
98135
[PatchFlags.NEED_PATCH]: `NEED_PATCH`,
136+
[PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`,
137+
[PatchFlags.DEV_ROOT_FRAGMENT]: `DEV_ROOT_FRAGMENT`,
99138
[PatchFlags.HOISTED]: `HOISTED`,
100139
[PatchFlags.BAIL]: `BAIL`
101140
}

0 commit comments

Comments
 (0)