fix(compiler): force block for custom dirs and inline beforeUpdate hooks

to ensure they are called before children updates
This commit is contained in:
Evan You 2021-12-10 15:34:23 +08:00
parent 4b5d1ac894
commit 1c9a4810fc
8 changed files with 82 additions and 21 deletions

View File

@ -67,13 +67,13 @@ exports[`compiler: transform text element with custom directives and only one te
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveDirective: _resolveDirective, openBlock: _openBlock, createElementBlock: _createElementBlock, withDirectives: _withDirectives } = _Vue
const _directive_foo = _resolveDirective(\\"foo\\") const _directive_foo = _resolveDirective(\\"foo\\")
return _withDirectives((_openBlock(), _createElementBlock(\\"p\\", null, [ return _withDirectives((_openBlock(), _createElementBlock(\\"p\\", null, [
_createTextVNode(_toDisplayString(foo), 1 /* TEXT */) _createTextVNode(_toDisplayString(foo), 1 /* TEXT */)
], 512 /* NEED_PATCH */)), [ ])), [
[_directive_foo] [_directive_foo]
]) ])
} }

View File

@ -1202,6 +1202,23 @@ describe('compiler: element transform', () => {
}) })
}) })
test('force block for runtime custom directive w/ children', () => {
const { node } = parseWithElementTransform(`<div v-foo>hello</div>`)
expect(node.isBlock).toBe(true)
})
test('force block for inline before-update handlers w/ children', () => {
expect(
parseWithElementTransform(`<div @vnode-before-update>hello</div>`).node
.isBlock
).toBe(true)
expect(
parseWithElementTransform(`<div @vnodeBeforeUpdate>hello</div>`).node
.isBlock
).toBe(true)
})
// #938 // #938
test('element with dynamic keys should be forced into blocks', () => { test('element with dynamic keys should be forced into blocks', () => {
const ast = parse(`<div><div :key="foo" /></div>`) const ast = parse(`<div><div :key="foo" /></div>`)

View File

@ -11,7 +11,7 @@ import {
advancePositionWithMutation, advancePositionWithMutation,
advancePositionWithClone, advancePositionWithClone,
isCoreComponent, isCoreComponent,
isBindKey isStaticArgOf
} from './utils' } from './utils'
import { import {
Namespaces, Namespaces,
@ -681,7 +681,7 @@ function isComponent(
} else if ( } else if (
// :is on plain element - only treat as component in compat mode // :is on plain element - only treat as component in compat mode
p.name === 'bind' && p.name === 'bind' &&
isBindKey(p.arg, 'is') && isStaticArgOf(p.arg, 'is') &&
__COMPAT__ && __COMPAT__ &&
checkCompatEnabled( checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,

View File

@ -168,6 +168,13 @@ export function getConstantType(
if (codegenNode.type !== NodeTypes.VNODE_CALL) { if (codegenNode.type !== NodeTypes.VNODE_CALL) {
return ConstantTypes.NOT_CONSTANT return ConstantTypes.NOT_CONSTANT
} }
if (
codegenNode.isBlock &&
node.tag !== 'svg' &&
node.tag !== 'foreignObject'
) {
return ConstantTypes.NOT_CONSTANT
}
const flag = getPatchFlag(codegenNode) const flag = getPatchFlag(codegenNode)
if (!flag) { if (!flag) {
let returnType = ConstantTypes.CAN_STRINGIFY let returnType = ConstantTypes.CAN_STRINGIFY

View File

@ -56,7 +56,7 @@ import {
toValidAssetId, toValidAssetId,
findProp, findProp,
isCoreComponent, isCoreComponent,
isBindKey, isStaticArgOf,
findDir, findDir,
isStaticExp isStaticExp
} from '../utils' } from '../utils'
@ -120,10 +120,7 @@ export const transformElement: NodeTransform = (node, context) => {
// updates inside get proper isSVG flag at runtime. (#639, #643) // updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core // This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity. // leads to too much unnecessary complexity.
(tag === 'svg' || (tag === 'svg' || tag === 'foreignObject'))
tag === 'foreignObject' ||
// #938: elements with dynamic keys should be forced into blocks
findProp(node, 'key', true)))
// props // props
if (props.length > 0) { if (props.length > 0) {
@ -138,6 +135,10 @@ export const transformElement: NodeTransform = (node, context) => {
directives.map(dir => buildDirectiveArgs(dir, context)) directives.map(dir => buildDirectiveArgs(dir, context))
) as DirectiveArguments) ) as DirectiveArguments)
: undefined : undefined
if (propsBuildResult.shouldUseBlock) {
shouldUseBlock = true
}
} }
// children // children
@ -386,12 +387,15 @@ export function buildProps(
directives: DirectiveNode[] directives: DirectiveNode[]
patchFlag: number patchFlag: number
dynamicPropNames: string[] dynamicPropNames: string[]
shouldUseBlock: boolean
} { } {
const { tag, loc: elementLoc } = node const { tag, loc: elementLoc, children } = node
const isComponent = node.tagType === ElementTypes.COMPONENT const isComponent = node.tagType === ElementTypes.COMPONENT
let properties: ObjectExpression['properties'] = [] let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = [] const mergeArgs: PropsExpression[] = []
const runtimeDirectives: DirectiveNode[] = [] const runtimeDirectives: DirectiveNode[] = []
const hasChildren = children.length > 0
let shouldUseBlock = false
// patchFlag analysis // patchFlag analysis
let patchFlag = 0 let patchFlag = 0
@ -526,7 +530,7 @@ export function buildProps(
if ( if (
name === 'is' || name === 'is' ||
(isVBind && (isVBind &&
isBindKey(arg, 'is') && isStaticArgOf(arg, 'is') &&
(isComponentTag(tag) || (isComponentTag(tag) ||
(__COMPAT__ && (__COMPAT__ &&
isCompatEnabled( isCompatEnabled(
@ -541,6 +545,16 @@ export function buildProps(
continue continue
} }
if (
// #938: elements with dynamic keys should be forced into blocks
(isVBind && isStaticArgOf(arg, 'key')) ||
// inline before-update hooks need to force block so that it is invoked
// before children
(isVOn && hasChildren && isStaticArgOf(arg, 'vnodeBeforeUpdate', true))
) {
shouldUseBlock = true
}
// special case for v-bind and v-on with no argument // special case for v-bind and v-on with no argument
if (!arg && (isVBind || isVOn)) { if (!arg && (isVBind || isVOn)) {
hasDynamicKeys = true hasDynamicKeys = true
@ -633,6 +647,11 @@ export function buildProps(
} else { } else {
// no built-in transform, this is a user custom directive. // no built-in transform, this is a user custom directive.
runtimeDirectives.push(prop) runtimeDirectives.push(prop)
// custom dirs may use beforeUpdate so they need to force blocks
// to ensure before-update gets called before children update
if (hasChildren) {
shouldUseBlock = true
}
} }
} }
@ -700,6 +719,7 @@ export function buildProps(
} }
} }
if ( if (
!shouldUseBlock &&
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) && (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
(hasRef || hasVnodeHook || runtimeDirectives.length > 0) (hasRef || hasVnodeHook || runtimeDirectives.length > 0)
) { ) {
@ -784,7 +804,8 @@ export function buildProps(
props: propsExpression, props: propsExpression,
directives: runtimeDirectives, directives: runtimeDirectives,
patchFlag, patchFlag,
dynamicPropNames dynamicPropNames,
shouldUseBlock
} }
} }

View File

@ -7,7 +7,7 @@ import {
SlotOutletNode, SlotOutletNode,
createFunctionExpression createFunctionExpression
} from '../ast' } from '../ast'
import { isSlotOutlet, isBindKey, isStaticExp } from '../utils' import { isSlotOutlet, isStaticArgOf, isStaticExp } from '../utils'
import { buildProps, PropsExpression } from './transformElement' import { buildProps, PropsExpression } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { RENDER_SLOT } from '../runtimeHelpers' import { RENDER_SLOT } from '../runtimeHelpers'
@ -75,7 +75,7 @@ export function processSlotOutlet(
} }
} }
} else { } else {
if (p.name === 'bind' && isBindKey(p.arg, 'name')) { if (p.name === 'bind' && isStaticArgOf(p.arg, 'name')) {
if (p.exp) slotName = p.exp if (p.exp) slotName = p.exp
} else { } else {
if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) { if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) {

View File

@ -42,7 +42,14 @@ import {
WITH_MEMO, WITH_MEMO,
OPEN_BLOCK OPEN_BLOCK
} from './runtimeHelpers' } from './runtimeHelpers'
import { isString, isObject, hyphenate, extend, NOOP } from '@vue/shared' import {
isString,
isObject,
hyphenate,
extend,
NOOP,
camelize
} from '@vue/shared'
import { PropsExpression } from './transforms/transformElement' import { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser' import { parseExpression } from '@babel/parser'
import { Expression } from '@babel/types' import { Expression } from '@babel/types'
@ -282,15 +289,23 @@ export function findProp(
} else if ( } else if (
p.name === 'bind' && p.name === 'bind' &&
(p.exp || allowEmpty) && (p.exp || allowEmpty) &&
isBindKey(p.arg, name) isStaticArgOf(p.arg, name)
) { ) {
return p return p
} }
} }
} }
export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean { export function isStaticArgOf(
return !!(arg && isStaticExp(arg) && arg.content === name) arg: DirectiveNode['arg'],
name: string,
camel?: boolean
): boolean {
return !!(
arg &&
isStaticExp(arg) &&
(camel ? camelize(arg.content) : arg.content) === name
)
} }
export function hasDynamicKeyVBind(node: ElementNode): boolean { export function hasDynamicKeyVBind(node: ElementNode): boolean {
@ -371,7 +386,8 @@ export function injectProp(
* *
* we need to get the real props before normalization * we need to get the real props before normalization
*/ */
let props = node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2] let props =
node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
let callPath: CallExpression[] = [] let callPath: CallExpression[] = []
let parentCall: CallExpression | undefined let parentCall: CallExpression | undefined
if ( if (

View File

@ -22,7 +22,7 @@ import {
TextNode, TextNode,
hasDynamicKeyVBind, hasDynamicKeyVBind,
MERGE_PROPS, MERGE_PROPS,
isBindKey, isStaticArgOf,
createSequenceExpression, createSequenceExpression,
InterpolationNode, InterpolationNode,
isStaticExp, isStaticExp,
@ -335,7 +335,7 @@ function isTextareaWithValue(
return !!( return !!(
node.tag === 'textarea' && node.tag === 'textarea' &&
prop.name === 'bind' && prop.name === 'bind' &&
isBindKey(prop.arg, 'value') isStaticArgOf(prop.arg, 'value')
) )
} }