Skip to content

Commit 1c9a481

Browse files
committedDec 10, 2021
fix(compiler): force block for custom dirs and inline beforeUpdate hooks
to ensure they are called before children updates
1 parent 4b5d1ac commit 1c9a481

File tree

8 files changed

+82
-21
lines changed

8 files changed

+82
-21
lines changed
 

‎packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ exports[`compiler: transform text element with custom directives and only one te
6767
6868
return function render(_ctx, _cache) {
6969
with (_ctx) {
70-
const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
70+
const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveDirective: _resolveDirective, openBlock: _openBlock, createElementBlock: _createElementBlock, withDirectives: _withDirectives } = _Vue
7171
7272
const _directive_foo = _resolveDirective(\\"foo\\")
7373
7474
return _withDirectives((_openBlock(), _createElementBlock(\\"p\\", null, [
7575
_createTextVNode(_toDisplayString(foo), 1 /* TEXT */)
76-
], 512 /* NEED_PATCH */)), [
76+
])), [
7777
[_directive_foo]
7878
])
7979
}

‎packages/compiler-core/__tests__/transforms/transformElement.spec.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,23 @@ describe('compiler: element transform', () => {
12021202
})
12031203
})
12041204

1205+
test('force block for runtime custom directive w/ children', () => {
1206+
const { node } = parseWithElementTransform(`<div v-foo>hello</div>`)
1207+
expect(node.isBlock).toBe(true)
1208+
})
1209+
1210+
test('force block for inline before-update handlers w/ children', () => {
1211+
expect(
1212+
parseWithElementTransform(`<div @vnode-before-update>hello</div>`).node
1213+
.isBlock
1214+
).toBe(true)
1215+
1216+
expect(
1217+
parseWithElementTransform(`<div @vnodeBeforeUpdate>hello</div>`).node
1218+
.isBlock
1219+
).toBe(true)
1220+
})
1221+
12051222
// #938
12061223
test('element with dynamic keys should be forced into blocks', () => {
12071224
const ast = parse(`<div><div :key="foo" /></div>`)

‎packages/compiler-core/src/parse.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
advancePositionWithMutation,
1212
advancePositionWithClone,
1313
isCoreComponent,
14-
isBindKey
14+
isStaticArgOf
1515
} from './utils'
1616
import {
1717
Namespaces,
@@ -681,7 +681,7 @@ function isComponent(
681681
} else if (
682682
// :is on plain element - only treat as component in compat mode
683683
p.name === 'bind' &&
684-
isBindKey(p.arg, 'is') &&
684+
isStaticArgOf(p.arg, 'is') &&
685685
__COMPAT__ &&
686686
checkCompatEnabled(
687687
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,

‎packages/compiler-core/src/transforms/hoistStatic.ts

+7
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ export function getConstantType(
168168
if (codegenNode.type !== NodeTypes.VNODE_CALL) {
169169
return ConstantTypes.NOT_CONSTANT
170170
}
171+
if (
172+
codegenNode.isBlock &&
173+
node.tag !== 'svg' &&
174+
node.tag !== 'foreignObject'
175+
) {
176+
return ConstantTypes.NOT_CONSTANT
177+
}
171178
const flag = getPatchFlag(codegenNode)
172179
if (!flag) {
173180
let returnType = ConstantTypes.CAN_STRINGIFY

‎packages/compiler-core/src/transforms/transformElement.ts

+29-8
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import {
5656
toValidAssetId,
5757
findProp,
5858
isCoreComponent,
59-
isBindKey,
59+
isStaticArgOf,
6060
findDir,
6161
isStaticExp
6262
} from '../utils'
@@ -120,10 +120,7 @@ export const transformElement: NodeTransform = (node, context) => {
120120
// updates inside get proper isSVG flag at runtime. (#639, #643)
121121
// This is technically web-specific, but splitting the logic out of core
122122
// leads to too much unnecessary complexity.
123-
(tag === 'svg' ||
124-
tag === 'foreignObject' ||
125-
// #938: elements with dynamic keys should be forced into blocks
126-
findProp(node, 'key', true)))
123+
(tag === 'svg' || tag === 'foreignObject'))
127124

128125
// props
129126
if (props.length > 0) {
@@ -138,6 +135,10 @@ export const transformElement: NodeTransform = (node, context) => {
138135
directives.map(dir => buildDirectiveArgs(dir, context))
139136
) as DirectiveArguments)
140137
: undefined
138+
139+
if (propsBuildResult.shouldUseBlock) {
140+
shouldUseBlock = true
141+
}
141142
}
142143

143144
// children
@@ -386,12 +387,15 @@ export function buildProps(
386387
directives: DirectiveNode[]
387388
patchFlag: number
388389
dynamicPropNames: string[]
390+
shouldUseBlock: boolean
389391
} {
390-
const { tag, loc: elementLoc } = node
392+
const { tag, loc: elementLoc, children } = node
391393
const isComponent = node.tagType === ElementTypes.COMPONENT
392394
let properties: ObjectExpression['properties'] = []
393395
const mergeArgs: PropsExpression[] = []
394396
const runtimeDirectives: DirectiveNode[] = []
397+
const hasChildren = children.length > 0
398+
let shouldUseBlock = false
395399

396400
// patchFlag analysis
397401
let patchFlag = 0
@@ -526,7 +530,7 @@ export function buildProps(
526530
if (
527531
name === 'is' ||
528532
(isVBind &&
529-
isBindKey(arg, 'is') &&
533+
isStaticArgOf(arg, 'is') &&
530534
(isComponentTag(tag) ||
531535
(__COMPAT__ &&
532536
isCompatEnabled(
@@ -541,6 +545,16 @@ export function buildProps(
541545
continue
542546
}
543547

548+
if (
549+
// #938: elements with dynamic keys should be forced into blocks
550+
(isVBind && isStaticArgOf(arg, 'key')) ||
551+
// inline before-update hooks need to force block so that it is invoked
552+
// before children
553+
(isVOn && hasChildren && isStaticArgOf(arg, 'vnodeBeforeUpdate', true))
554+
) {
555+
shouldUseBlock = true
556+
}
557+
544558
// special case for v-bind and v-on with no argument
545559
if (!arg && (isVBind || isVOn)) {
546560
hasDynamicKeys = true
@@ -633,6 +647,11 @@ export function buildProps(
633647
} else {
634648
// no built-in transform, this is a user custom directive.
635649
runtimeDirectives.push(prop)
650+
// custom dirs may use beforeUpdate so they need to force blocks
651+
// to ensure before-update gets called before children update
652+
if (hasChildren) {
653+
shouldUseBlock = true
654+
}
636655
}
637656
}
638657

@@ -700,6 +719,7 @@ export function buildProps(
700719
}
701720
}
702721
if (
722+
!shouldUseBlock &&
703723
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
704724
(hasRef || hasVnodeHook || runtimeDirectives.length > 0)
705725
) {
@@ -784,7 +804,8 @@ export function buildProps(
784804
props: propsExpression,
785805
directives: runtimeDirectives,
786806
patchFlag,
787-
dynamicPropNames
807+
dynamicPropNames,
808+
shouldUseBlock
788809
}
789810
}
790811

‎packages/compiler-core/src/transforms/transformSlotOutlet.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
SlotOutletNode,
88
createFunctionExpression
99
} from '../ast'
10-
import { isSlotOutlet, isBindKey, isStaticExp } from '../utils'
10+
import { isSlotOutlet, isStaticArgOf, isStaticExp } from '../utils'
1111
import { buildProps, PropsExpression } from './transformElement'
1212
import { createCompilerError, ErrorCodes } from '../errors'
1313
import { RENDER_SLOT } from '../runtimeHelpers'
@@ -75,7 +75,7 @@ export function processSlotOutlet(
7575
}
7676
}
7777
} else {
78-
if (p.name === 'bind' && isBindKey(p.arg, 'name')) {
78+
if (p.name === 'bind' && isStaticArgOf(p.arg, 'name')) {
7979
if (p.exp) slotName = p.exp
8080
} else {
8181
if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) {

‎packages/compiler-core/src/utils.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@ import {
4242
WITH_MEMO,
4343
OPEN_BLOCK
4444
} from './runtimeHelpers'
45-
import { isString, isObject, hyphenate, extend, NOOP } from '@vue/shared'
45+
import {
46+
isString,
47+
isObject,
48+
hyphenate,
49+
extend,
50+
NOOP,
51+
camelize
52+
} from '@vue/shared'
4653
import { PropsExpression } from './transforms/transformElement'
4754
import { parseExpression } from '@babel/parser'
4855
import { Expression } from '@babel/types'
@@ -282,15 +289,23 @@ export function findProp(
282289
} else if (
283290
p.name === 'bind' &&
284291
(p.exp || allowEmpty) &&
285-
isBindKey(p.arg, name)
292+
isStaticArgOf(p.arg, name)
286293
) {
287294
return p
288295
}
289296
}
290297
}
291298

292-
export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
293-
return !!(arg && isStaticExp(arg) && arg.content === name)
299+
export function isStaticArgOf(
300+
arg: DirectiveNode['arg'],
301+
name: string,
302+
camel?: boolean
303+
): boolean {
304+
return !!(
305+
arg &&
306+
isStaticExp(arg) &&
307+
(camel ? camelize(arg.content) : arg.content) === name
308+
)
294309
}
295310

296311
export function hasDynamicKeyVBind(node: ElementNode): boolean {
@@ -371,7 +386,8 @@ export function injectProp(
371386
*
372387
* we need to get the real props before normalization
373388
*/
374-
let props = node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
389+
let props =
390+
node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
375391
let callPath: CallExpression[] = []
376392
let parentCall: CallExpression | undefined
377393
if (

‎packages/compiler-ssr/src/transforms/ssrTransformElement.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
TextNode,
2323
hasDynamicKeyVBind,
2424
MERGE_PROPS,
25-
isBindKey,
25+
isStaticArgOf,
2626
createSequenceExpression,
2727
InterpolationNode,
2828
isStaticExp,
@@ -335,7 +335,7 @@ function isTextareaWithValue(
335335
return !!(
336336
node.tag === 'textarea' &&
337337
prop.name === 'bind' &&
338-
isBindKey(prop.arg, 'value')
338+
isStaticArgOf(prop.arg, 'value')
339339
)
340340
}
341341

0 commit comments

Comments
 (0)
Please sign in to comment.