feat(compiler): generate patchFlags for runtime

This commit is contained in:
Evan You 2019-09-30 21:17:12 -04:00
parent da0d785d84
commit d67418002f
19 changed files with 267 additions and 70 deletions

View File

@ -11,14 +11,14 @@ return function render() {
}, [
_toString(world.burn()),
ok
? _createVNode(\\"div\\", 0, \\"yes\\")
? _createVNode(\\"div\\", null, \\"yes\\")
: \\"no\\",
_renderList(list, (value, index) => {
return _createVNode(\\"div\\", 0, [
_createVNode(\\"span\\", 0, _toString(value + index))
return _createVNode(\\"div\\", null, [
_createVNode(\\"span\\", null, _toString(value + index))
])
})
])
], 2)
}
}"
`;
@ -34,14 +34,14 @@ return function render() {
}, [
toString(_ctx.world.burn()),
(_ctx.ok)
? createVNode(\\"div\\", 0, \\"yes\\")
? createVNode(\\"div\\", null, \\"yes\\")
: \\"no\\",
renderList(_ctx.list, (value, index) => {
return createVNode(\\"div\\", 0, [
createVNode(\\"span\\", 0, toString(value + index))
return createVNode(\\"div\\", null, [
createVNode(\\"span\\", null, toString(value + index))
])
})
])
], 2)
}"
`;
@ -56,13 +56,13 @@ export default function render() {
}, [
_toString(_ctx.world.burn()),
(_ctx.ok)
? createVNode(\\"div\\", 0, \\"yes\\")
? createVNode(\\"div\\", null, \\"yes\\")
: \\"no\\",
_renderList(_ctx.list, (value, index) => {
return createVNode(\\"div\\", 0, [
createVNode(\\"span\\", 0, _toString(value + index))
return createVNode(\\"div\\", null, [
createVNode(\\"span\\", null, _toString(value + index))
])
})
])
], 2)
}"
`;

View File

@ -7,7 +7,7 @@ return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
return createVNode(_component_Comp, null, {
[_ctx.one]: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
@ -16,7 +16,7 @@ return function render() {
toString(_ctx.foo),
toString(bar)
]
})
}, 256)
}"
`;
@ -27,7 +27,7 @@ return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
return createVNode(_component_Comp, null, {
default: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
@ -43,7 +43,7 @@ return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
return createVNode(_component_Comp, null, {
default: () => [
createVNode(\\"div\\")
]
@ -58,7 +58,7 @@ return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
return createVNode(_component_Comp, null, {
one: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
@ -79,9 +79,9 @@ return function render() {
const _component_Comp = resolveComponent(\\"Comp\\")
const _component_Inner = resolveComponent(\\"Inner\\")
return createVNode(_component_Comp, 0, {
return createVNode(_component_Comp, null, {
default: ({ foo }) => [
createVNode(_component_Inner, 0, {
createVNode(_component_Inner, null, {
default: ({ bar }) => [
toString(foo),
toString(bar),

View File

@ -24,6 +24,7 @@ import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn'
import { transformStyle } from '../../src/transforms/transformStyle'
import { transformBind } from '../../src/transforms/vBind'
import { PatchFlags } from '@vue/shared'
function parseWithElementTransform(
template: string,
@ -127,7 +128,7 @@ describe('compiler: element transform', () => {
expect(node.callee).toBe(`_${CREATE_VNODE}`)
expect(node.arguments).toMatchObject([
`"div"`,
`0`,
`null`,
[
{
type: NodeTypes.ELEMENT,
@ -351,7 +352,9 @@ describe('compiler: element transform', () => {
value: _dir!.exp
}
]
}
},
`null`,
String(PatchFlags.NEED_PATCH) // should generate appropriate flag
]
},
{
@ -546,5 +549,121 @@ describe('compiler: element transform', () => {
})
})
test.todo('slot outlets')
test(`props merging: class`, () => {
const { node } = parseWithElementTransform(
`<div class="foo" :class="{ bar: isBar }" />`,
{
directiveTransforms: {
bind: transformBind
}
}
)
expect(node.arguments[1]).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
type: NodeTypes.JS_PROPERTY,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `class`,
isStatic: true
},
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `foo`,
isStatic: true
},
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ bar: isBar }`,
isStatic: false
}
]
}
}
]
})
})
describe('patchFlag analysis', () => {
function parseWithBind(template: string) {
return parseWithElementTransform(template, {
directiveTransforms: {
bind: transformBind
}
})
}
test('CLASS', () => {
const { node } = parseWithBind(`<div :class="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(String(PatchFlags.CLASS))
})
test('STYLE', () => {
const { node } = parseWithBind(`<div :style="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(String(PatchFlags.STYLE))
})
test('PROPS', () => {
const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`)
expect(node.arguments.length).toBe(5)
expect(node.arguments[3]).toBe(String(PatchFlags.PROPS))
expect(node.arguments[4]).toBe(`["foo", "baz"]`)
})
test('CLASS + STYLE + PROPS', () => {
const { node } = parseWithBind(
`<div id="foo" :class="cls" :style="styl" :foo="bar" :baz="qux"/>`
)
expect(node.arguments.length).toBe(5)
expect(node.arguments[3]).toBe(
String(PatchFlags.PROPS | PatchFlags.CLASS | PatchFlags.STYLE)
)
expect(node.arguments[4]).toBe(`["foo", "baz"]`)
})
test('FULL_PROPS (v-bind)', () => {
const { node } = parseWithBind(`<div v-bind="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(String(PatchFlags.FULL_PROPS))
})
test('FULL_PROPS (dynamic key)', () => {
const { node } = parseWithBind(`<div :[foo]="bar" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(String(PatchFlags.FULL_PROPS))
})
test('FULL_PROPS (w/ others)', () => {
const { node } = parseWithBind(
`<div id="foo" v-bind="bar" :class="cls" />`
)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(String(PatchFlags.FULL_PROPS))
})
test('NEED_PATCH (static ref)', () => {
const { node } = parseWithBind(`<div ref="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(String(PatchFlags.NEED_PATCH))
})
test('NEED_PATCH (dynamic ref)', () => {
const { node } = parseWithBind(`<div :ref="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(String(PatchFlags.NEED_PATCH))
})
test('NEED_PATCH (custom directives)', () => {
const { node } = parseWithBind(`<div v-foo />`)
const vnodeCall = node.arguments[0] as CallExpression
expect(vnodeCall.arguments.length).toBe(4)
expect(vnodeCall.arguments[3]).toBe(String(PatchFlags.NEED_PATCH))
})
})
})

View File

@ -260,7 +260,7 @@ describe('compiler: transform component slots', () => {
type: NodeTypes.JS_CALL_EXPRESSION,
arguments: [
`_component_Inner`,
`0`,
`null`,
createSlotMatcher({
default: {
type: NodeTypes.JS_SLOT_FUNCTION,

View File

@ -169,7 +169,7 @@ export type JSChildNode =
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
callee: string | ExpressionNode
callee: string
arguments: (string | JSChildNode | ChildNode[])[]
}
@ -268,7 +268,7 @@ export function createCompoundExpression(
}
export function createCallExpression(
callee: string | ExpressionNode,
callee: string,
args: CallExpression['arguments'],
loc: SourceLocation
): CallExpression {

View File

@ -544,12 +544,7 @@ function genCallExpression(
context: CodegenContext,
multilines = node.arguments.length > 2
) {
if (isString(node.callee)) {
context.push(node.callee + `(`, node, true)
} else {
genNode(node.callee, context)
context.push(`(`)
}
context.push(node.callee + `(`, node, true)
multilines && context.indent()
genNodeList(node.arguments, context, multilines)
multilines && context.deindent()

View File

@ -8,7 +8,8 @@ import {
Property,
ExpressionNode,
createSimpleExpression,
JSChildNode
JSChildNode,
SimpleExpressionNode
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
@ -65,7 +66,7 @@ export interface TransformContext extends Required<TransformOptions> {
onNodeRemoved: () => void
addIdentifiers(exp: ExpressionNode): void
removeIdentifiers(exp: ExpressionNode): void
hoist(exp: JSChildNode): ExpressionNode
hoist(exp: JSChildNode): SimpleExpressionNode
}
function createTransformContext(

View File

@ -16,7 +16,7 @@ import {
Property,
SourceLocation
} from '../ast'
import { isArray } from '@vue/shared'
import { isArray, PatchFlags } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import {
CREATE_VNODE,
@ -41,7 +41,9 @@ export const transformElement: NodeTransform = (node, context) => {
const isComponent = node.tagType === ElementTypes.COMPONENT
let hasProps = node.props.length > 0
const hasChildren = node.children.length > 0
let patchFlag: number = 0
let runtimeDirectives: DirectiveNode[] | undefined
let dynamicPropNames: string[] | undefined
let componentIdentifier: string | undefined
if (isComponent) {
@ -58,26 +60,51 @@ export const transformElement: NodeTransform = (node, context) => {
]
// props
if (hasProps) {
const { props, directives } = buildProps(
const propsBuildResult = buildProps(
node.props,
node.loc,
context,
isComponent
)
runtimeDirectives = directives
if (!props) {
patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames
runtimeDirectives = propsBuildResult.directives
if (!propsBuildResult.props) {
hasProps = false
} else {
args.push(props)
args.push(propsBuildResult.props)
}
}
// children
if (hasChildren) {
if (!hasProps) {
// placeholder for null props, but use `0` for more condense code
args.push(`0`)
args.push(`null`)
}
if (isComponent) {
const { slots, hasDynamicSlotName } = buildSlots(node, context)
args.push(slots)
if (hasDynamicSlotName) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
} else {
// only v-for fragments will have keyed/unkeyed flags
args.push(node.children)
}
}
// patchFlag & dynamicPropNames
if (patchFlag !== 0) {
if (!hasChildren) {
if (!hasProps) {
args.push(`null`)
}
args.push(`null`)
}
args.push(String(patchFlag))
if (dynamicPropNames && dynamicPropNames.length) {
args.push(
`[${dynamicPropNames.map(n => JSON.stringify(n)).join(`, `)}]`
)
}
args.push(isComponent ? buildSlots(node, context) : node.children)
}
const { loc } = node
@ -118,17 +145,30 @@ export function buildProps(
): {
props: PropsExpression | undefined
directives: DirectiveNode[]
patchFlag: number
dynamicPropNames: string[]
} {
let isStatic = true
let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = []
const runtimeDirectives: DirectiveNode[] = []
// patchFlag analysis
let patchFlag = 0
const dynamicPropNames: string[] = []
let hasDynammicKeys = false
let hasClassBinding = false
let hasStyleBinding = false
let hasRef = false
for (let i = 0; i < props.length; i++) {
// static attribute
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
const { loc, name, value } = prop
if (name === 'ref') {
hasRef = true
}
properties.push(
createObjectProperty(
createSimpleExpression(
@ -162,6 +202,7 @@ export function buildProps(
// special case for v-bind and v-on with no argument
const isBind = name === 'bind'
if (!arg && (isBind || name === 'on')) {
hasDynammicKeys = true
if (exp) {
if (properties.length) {
mergeArgs.push(
@ -193,6 +234,24 @@ export function buildProps(
continue
}
// patchFlag analysis
if (isBind && arg) {
if (arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic) {
const name = arg.content
if (name === 'ref') {
hasRef = true
} else if (name === 'class') {
hasClassBinding = true
} else if (name === 'style') {
hasStyleBinding = true
} else {
dynamicPropNames.push(name)
}
} else {
hasDynammicKeys = true
}
}
const directiveTransform = context.directiveTransforms[name]
if (directiveTransform) {
// has built-in directive transform.
@ -243,9 +302,29 @@ export function buildProps(
propsExpression = context.hoist(propsExpression)
}
// determine the flags to add
if (hasDynammicKeys) {
patchFlag |= PatchFlags.FULL_PROPS
} else {
if (hasClassBinding) {
patchFlag |= PatchFlags.CLASS
}
if (hasStyleBinding) {
patchFlag |= PatchFlags.STYLE
}
if (dynamicPropNames.length) {
patchFlag |= PatchFlags.PROPS
}
}
if (patchFlag === 0 && (hasRef || runtimeDirectives.length > 0)) {
patchFlag |= PatchFlags.NEED_PATCH
}
return {
props: propsExpression,
directives: runtimeDirectives
directives: runtimeDirectives,
patchFlag,
dynamicPropNames
}
}

View File

@ -44,8 +44,12 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
export function buildSlots(
{ props, children, loc }: ElementNode,
context: TransformContext
): ObjectExpression {
): {
slots: ObjectExpression
hasDynamicSlotName: boolean
} {
const slots: Property[] = []
let hasDynamicSlotName = false
// 1. Check for default slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/>
@ -83,11 +87,11 @@ export function buildSlots(
)
break
} else {
// check duplicate slot names
if (
!slotName ||
(slotName.type === NodeTypes.SIMPLE_EXPRESSION && slotName.isStatic)
) {
// check duplicate slot names
const name = slotName ? slotName.content : `default`
if (seenSlotNames.has(name)) {
context.onError(
@ -96,6 +100,8 @@ export function buildSlots(
continue
}
seenSlotNames.add(name)
} else {
hasDynamicSlotName = true
}
slots.push(
buildSlot(slotName || `default`, slotProps, children, nodeLoc)
@ -120,7 +126,10 @@ export function buildSlots(
slots.push(buildSlot(`default`, undefined, children, loc))
}
return createObjectExpression(slots, loc)
return {
slots: createObjectExpression(slots, loc),
hasDynamicSlotName
}
}
function buildSlot(

View File

@ -107,7 +107,7 @@ describe('renderer: fragment', () => {
it('patch fragment children (compiler generated, unkeyed)', () => {
const root = nodeOps.createElement('div')
render(
createVNode(Fragment, 0, [h('div', 'one'), 'two'], PatchFlags.UNKEYED),
createVNode(Fragment, null, [h('div', 'one'), 'two'], PatchFlags.UNKEYED),
root
)
expect(serializeInner(root)).toBe(`<!----><div>one</div>two<!---->`)
@ -115,7 +115,7 @@ describe('renderer: fragment', () => {
render(
createVNode(
Fragment,
0,
null,
[h('div', 'foo'), 'bar', 'baz'],
PatchFlags.UNKEYED
),
@ -130,7 +130,7 @@ describe('renderer: fragment', () => {
render(
createVNode(
Fragment,
0,
null,
[h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')],
PatchFlags.KEYED
),
@ -144,7 +144,7 @@ describe('renderer: fragment', () => {
render(
createVNode(
Fragment,
0,
null,
[h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')],
PatchFlags.KEYED
),

View File

@ -10,11 +10,11 @@ import {
isObject,
isReservedProp,
hasOwn,
toTypeString
toTypeString,
PatchFlags
} from '@vue/shared'
import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component'
import { PatchFlags } from './patchFlags'
export type ComponentPropsOptions<P = Data> = {
[K in keyof P]: Prop<P[K]> | null

View File

@ -6,7 +6,7 @@ import {
import { VNode, normalizeVNode, createVNode, Comment } from './vnode'
import { ShapeFlags } from './shapeFlags'
import { handleError, ErrorCodes } from './errorHandling'
import { PatchFlags } from './patchFlags'
import { PatchFlags } from '@vue/shared'
// mark the current rendering instance for asset resolution (e.g.
// resolveComponent, resolveDirective) during render

View File

@ -25,7 +25,8 @@ import {
EMPTY_ARR,
isReservedProp,
isFunction,
isArray
isArray,
PatchFlags
} from '@vue/shared'
import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler'
import {
@ -38,7 +39,6 @@ import {
} from '@vue/reactivity'
import { resolveProps } from './componentProps'
import { resolveSlots } from './componentSlots'
import { PatchFlags } from './patchFlags'
import { ShapeFlags } from './shapeFlags'
import { pushWarningContext, popWarningContext, warn } from './warning'
import { invokeDirectiveHook } from './directives'

View File

@ -21,8 +21,8 @@ export {
// VNode type symbols
export { Text, Comment, Fragment, Portal, Suspense } from './vnode'
// VNode flags
export { PublicPatchFlags as PatchFlags } from './patchFlags'
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
export { PublicPatchFlags as PatchFlags } from '@vue/shared'
// For advanced plugins
export { getCurrentInstance } from './component'

View File

@ -4,11 +4,11 @@ import {
isString,
isObject,
EMPTY_ARR,
extend
extend,
PatchFlags
} from '@vue/shared'
import { ComponentInternalInstance, Data, SetupProxySymbol } from './component'
import { RawSlots } from './componentSlots'
import { PatchFlags } from './patchFlags'
import { ShapeFlags } from './shapeFlags'
import { isReactive } from '@vue/reactivity'
import { AppContext } from './apiApp'
@ -131,14 +131,11 @@ export function isVNode(value: any): boolean {
export function createVNode(
type: VNodeTypes,
props: { [key: string]: any } | null | 0 = null,
children: any = null,
props: { [key: string]: any } | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null
): VNode {
// Allow passing 0 for props, this can save bytes on generated code.
props = props || null
// class & style normalization.
if (props !== null) {
// for reactive or proxy objects, we need to clone it to enable mutation.

View File

@ -1,3 +1,5 @@
export * from './patchFlags'
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})
: {}

View File

@ -17,12 +17,7 @@ export const enum PatchFlags {
// Indicates an element with dynamic textContent (children fast path)
TEXT = 1,
// Indicates an element with dynamic class.
// The compiler also pre-normalizes the :class binding:
// - b -> normalize(b)
// - ['foo', b] -> 'foo' + normalize(b)
// - { a, b: c } -> (a ? a : '') + (b ? c : '')
// - ['a', b, { c }] -> 'a' + normalize(b) + (c ? c : '')
// Indicates an element with dynamic class binding.
CLASS = 1 << 1,
// Indicates an element with dynamic style
@ -48,6 +43,8 @@ export const enum PatchFlags {
// Indicates an element that only needs non-props patching, e.g. ref or
// directives (vnodeXXX hooks). It simply marks the vnode as "need patch",
// since every pathced vnode checks for refs and vnodeXXX hooks.
// This flag is never directly matched against, it simply serves as a non-zero
// value.
NEED_PATCH = 1 << 5,
// Indicates a fragment or element with keyed or partially-keyed v-for