feat(compiler): force dynamicSlots flag when inside v-for or v-slot

This commit is contained in:
Evan You 2019-10-03 16:27:46 -04:00
parent 4dea23f79e
commit c2fc7e3347
6 changed files with 73 additions and 16 deletions

View File

@ -159,7 +159,7 @@ return function render() {
createVNode(_component_Inner, null, { createVNode(_component_Inner, null, {
default: ({ bar }) => [toString(foo), toString(bar), toString(_ctx.baz)], default: ({ bar }) => [toString(foo), toString(bar), toString(_ctx.baz)],
_compiled: true _compiled: true
}), }, 256 /* DYNAMIC_SLOTS */),
toString(foo), toString(foo),
toString(_ctx.bar), toString(_ctx.bar),
toString(_ctx.baz) toString(_ctx.baz)

View File

@ -5,7 +5,8 @@ import {
generate, generate,
ElementNode, ElementNode,
NodeTypes, NodeTypes,
ErrorCodes ErrorCodes,
ForNode
} from '../../src' } from '../../src'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
@ -17,15 +18,20 @@ import {
} from '../../src/transforms/vSlot' } from '../../src/transforms/vSlot'
import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeConstants' import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeConstants'
import { createObjectMatcher } from '../testUtils' import { createObjectMatcher } from '../testUtils'
import { PatchFlags } from '@vue/shared' import { PatchFlags, PatchFlagNames } from '@vue/shared'
import { transformFor } from '../../src/transforms/vFor'
import { transformIf } from '../../src/transforms/vIf'
function parseWithSlots(template: string, options: CompilerOptions = {}) { function parseWithSlots(template: string, options: CompilerOptions = {}) {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [ nodeTransforms: [
transformIf,
transformFor,
...(options.prefixIdentifiers ...(options.prefixIdentifiers
? [trackVForSlotScopes, transformExpression, trackSlotScopes] ? [trackVForSlotScopes, transformExpression]
: []), : []),
trackSlotScopes,
transformElement transformElement
], ],
directiveTransforms: { directiveTransforms: {
@ -36,7 +42,10 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
}) })
return { return {
root: ast, root: ast,
slots: (ast.children[0] as ElementNode).codegenNode!.arguments[2] slots:
ast.children[0].type === NodeTypes.ELEMENT
? ast.children[0].codegenNode!.arguments[2]
: null
} }
} }
@ -295,7 +304,12 @@ describe('compiler: transform component slots', () => {
} }
] ]
} }
}) }),
// nested slot should be forced dynamic, since scope variables
// are not tracked as dependencies of the slot.
`${PatchFlags.DYNAMIC_SLOTS} /* ${
PatchFlagNames[PatchFlags.DYNAMIC_SLOTS]
} */`
] ]
} }
}, },
@ -325,6 +339,18 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
test('should force dynamic when inside v-for', () => {
const { root } = parseWithSlots(
`<div v-for="i in list">
<Comp v-slot="bar">foo</Comp>
</div>`
)
const div = ((root.children[0] as ForNode).children[0] as ElementNode)
.codegenNode as any
const comp = div.arguments[2][0]
expect(comp.codegenNode.arguments[3]).toMatch(PatchFlags.DYNAMIC_SLOTS + '')
})
test('named slot with v-if', () => { test('named slot with v-if', () => {
const { root, slots } = parseWithSlots( const { root, slots } = parseWithSlots(
`<Comp> `<Comp>

View File

@ -49,10 +49,10 @@ export function baseCompile(
? [ ? [
// order is important // order is important
trackVForSlotScopes, trackVForSlotScopes,
transformExpression, transformExpression
trackSlotScopes
] ]
: []), : []),
trackSlotScopes,
optimizeText, optimizeText,
transformStyle, transformStyle,
transformSlotOutlet, transformSlotOutlet,

View File

@ -59,6 +59,12 @@ export interface TransformContext extends Required<TransformOptions> {
statements: Set<string> statements: Set<string>
hoists: JSChildNode[] hoists: JSChildNode[]
identifiers: { [name: string]: number | undefined } identifiers: { [name: string]: number | undefined }
scopes: {
vFor: number
vSlot: number
vPre: number
vOnce: number
}
parent: ParentNode | null parent: ParentNode | null
childIndex: number childIndex: number
currentNode: RootNode | TemplateChildNode | null currentNode: RootNode | TemplateChildNode | null
@ -86,6 +92,12 @@ function createTransformContext(
statements: new Set(), statements: new Set(),
hoists: [], hoists: [],
identifiers: {}, identifiers: {},
scopes: {
vFor: 0,
vSlot: 0,
vPre: 0,
vOnce: 0
},
prefixIdentifiers, prefixIdentifiers,
nodeTransforms, nodeTransforms,
directiveTransforms, directiveTransforms,

View File

@ -46,7 +46,7 @@ export const transformFor = createStructuralDirectiveTransform(
) )
if (parseResult) { if (parseResult) {
const { helper, addIdentifiers, removeIdentifiers } = context const { helper, addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult const { source, value, key, index } = parseResult
// create the loop render function expression now, and add the // create the loop render function expression now, and add the
@ -79,6 +79,8 @@ export const transformFor = createStructuralDirectiveTransform(
codegenNode codegenNode
}) })
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) { if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management // scope management
// inject identifiers to context // inject identifiers to context
@ -88,6 +90,7 @@ export const transformFor = createStructuralDirectiveTransform(
} }
return () => { return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) { if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value) value && removeIdentifiers(value)
key && removeIdentifiers(key) key && removeIdentifiers(key)

View File

@ -32,22 +32,33 @@ const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
const defaultFallback = createSimpleExpression(`undefined`, false) const defaultFallback = createSimpleExpression(`undefined`, false)
// A NodeTransform that tracks scope identifiers for scoped slots so that they // A NodeTransform that:
// don't get prefixed by transformExpression. This transform is only applied // 1. Tracks scope identifiers for scoped slots so that they don't get prefixed
// in non-browser builds with { prefixIdentifiers: true } // by transformExpression. This is only applied in non-browser builds with
// { prefixIdentifiers: true }.
// 2. Track v-slot depths so that we know a slot is inside another slot.
// Note the exit callback is executed before buildSlots() on the same node,
// so only nested slots see positive numbers.
export const trackSlotScopes: NodeTransform = (node, context) => { export const trackSlotScopes: NodeTransform = (node, context) => {
if ( if (
node.type === NodeTypes.ELEMENT && node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.COMPONENT || (node.tagType === ElementTypes.COMPONENT ||
node.tagType === ElementTypes.TEMPLATE) node.tagType === ElementTypes.TEMPLATE)
) { ) {
// We are only checking non-empty v-slot here
// since we only care about slots that introduce scope variables.
const vSlot = findDir(node, 'slot') const vSlot = findDir(node, 'slot')
if (vSlot) { if (vSlot) {
const { addIdentifiers, removeIdentifiers } = context
const slotProps = vSlot.exp const slotProps = vSlot.exp
slotProps && addIdentifiers(slotProps) if (!__BROWSER__ && context.prefixIdentifiers) {
slotProps && context.addIdentifiers(slotProps)
}
context.scopes.vSlot++
return () => { return () => {
slotProps && removeIdentifiers(slotProps) if (!__BROWSER__ && context.prefixIdentifiers) {
slotProps && context.removeIdentifiers(slotProps)
}
context.scopes.vSlot--
} }
} }
} }
@ -94,7 +105,12 @@ export function buildSlots(
const { children, loc } = node const { children, loc } = node
const slotsProperties: Property[] = [] const slotsProperties: Property[] = []
const dynamicSlots: (ConditionalExpression | CallExpression)[] = [] const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
let hasDynamicSlots = false
// If the slot is inside a v-for or another v-slot, force it to be dynamic
// since it likely uses a scope variable.
// TODO: This can be further optimized to only make it dynamic when the slot
// actually uses the scope variables.
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
// 1. Check for default slot with slotProps on component itself. // 1. Check for default slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/> // <Comp v-slot="{ prop }"/>