feat(compiler-vapor): `v-else` / `v-else-if` (#98)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Rizumu Ayaka 2024-01-29 03:42:56 +08:00 committed by GitHub
parent 63aacf6194
commit 63a127b612
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 496 additions and 53 deletions

View File

@ -20,6 +20,30 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compiler: v-if > comment between branches 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<!--foo--><p></p>")
const t2 = _template("<!--bar-->fine")
const t3 = _fragment()
const n0 = t3()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => _createIf(() => (_ctx.orNot), () => {
const n3 = t1()
return n3
}, () => {
const n4 = t2()
return n4
}))
_prepend(n0, n1)
return n0
}"
`;
exports[`compiler: v-if > dedupe same template 1`] = ` exports[`compiler: v-if > dedupe same template 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, append as _append } from 'vue/vapor'; "import { template as _template, fragment as _fragment, createIf as _createIf, append as _append } from 'vue/vapor';
@ -59,3 +83,67 @@ export function render(_ctx) {
return n0 return n0
}" }"
`; `;
exports[`compiler: v-if > v-if + v-else 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<p></p>")
const t2 = _fragment()
const n0 = t2()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => {
const n3 = t1()
return n3
})
_prepend(n0, n1)
return n0
}"
`;
exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<p></p>")
const t2 = _template("fine")
const t3 = _fragment()
const n0 = t3()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => _createIf(() => (_ctx.orNot), () => {
const n3 = t1()
return n3
}, () => {
const n4 = t2()
return n4
}))
_prepend(n0, n1)
return n0
}"
`;
exports[`compiler: v-if > v-if + v-else-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<p></p>")
const t2 = _fragment()
const n0 = t2()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => _createIf(() => (_ctx.orNot), () => {
const n3 = t1()
return n3
}))
_prepend(n0, n1)
return n0
}"
`;

View File

@ -1,11 +1,14 @@
import { makeCompile } from './_utils' import { makeCompile } from './_utils'
import { import {
IRNodeTypes,
type IfIRNode,
transformElement, transformElement,
transformInterpolation, transformInterpolation,
transformOnce, transformOnce,
transformVIf, transformVIf,
transformVText, transformVText,
} from '../../src' } from '../../src'
import { NodeTypes } from '@vue/compiler-core'
const compileWithVIf = makeCompile({ const compileWithVIf = makeCompile({
nodeTransforms: [ nodeTransforms: [
@ -21,15 +24,87 @@ const compileWithVIf = makeCompile({
describe('compiler: v-if', () => { describe('compiler: v-if', () => {
test('basic v-if', () => { test('basic v-if', () => {
const { code } = compileWithVIf(`<div v-if="ok">{{msg}}</div>`) const { code, vaporHelpers, ir, helpers } = compileWithVIf(
`<div v-if="ok">{{msg}}</div>`,
)
expect(vaporHelpers).contains('createIf')
expect(helpers.size).toBe(0)
expect(ir.template).lengthOf(2)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
type: IRNodeTypes.FRAGMENT_FACTORY,
},
])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'ok',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
},
{
type: IRNodeTypes.APPEND_NODE,
elements: [1],
parent: 0,
},
])
expect(ir.dynamic).toMatchObject({
id: 0,
children: { 0: { id: 1 } },
})
expect(ir.effect).toEqual([])
expect((ir.operation[0] as IfIRNode).positive.effect).lengthOf(1)
expect(code).matchSnapshot() expect(code).matchSnapshot()
}) })
test('template v-if', () => { test('template v-if', () => {
const { code } = compileWithVIf( const { code, ir } = compileWithVIf(
`<template v-if="ok"><div/>hello<p v-text="msg"/></template>`, `<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
) )
expect(code).matchSnapshot() expect(code).matchSnapshot()
expect(ir.template).lengthOf(2)
expect(ir.template[0]).toMatchObject({
template: '<div></div>hello<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
})
expect(ir.effect).toEqual([])
expect((ir.operation[0] as IfIRNode).positive.effect).toMatchObject([
{
operations: [
{
type: IRNodeTypes.SET_TEXT,
element: 3,
value: {
content: 'msg',
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
},
},
],
},
])
expect((ir.operation[0] as IfIRNode).positive.dynamic).toMatchObject({
id: 2,
children: { 2: { id: 3 } },
})
}) })
test('dedupe same template', () => { test('dedupe same template', () => {
@ -42,10 +117,185 @@ describe('compiler: v-if', () => {
test.todo('v-if with v-once') test.todo('v-if with v-once')
test.todo('component v-if') test.todo('component v-if')
test.todo('v-if + v-else')
test.todo('v-if + v-else-if') test('v-if + v-else', () => {
test.todo('v-if + v-else-if + v-else') const { code, ir, vaporHelpers, helpers } = compileWithVIf(
test.todo('comment between branches') `<div v-if="ok"/><p v-else/>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(3)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
type: IRNodeTypes.FRAGMENT_FACTORY,
},
])
expect(vaporHelpers).contains('createIf')
expect(ir.effect).lengthOf(0)
expect(helpers).lengthOf(0)
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'ok',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
negative: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 1,
},
},
{
type: IRNodeTypes.PREPEND_NODE,
elements: [1],
parent: 0,
},
])
})
test('v-if + v-else-if', () => {
const { code, ir } = compileWithVIf(
`<div v-if="ok"/><p v-else-if="orNot"/>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(3)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{ type: IRNodeTypes.FRAGMENT_FACTORY },
])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'ok',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
negative: {
type: IRNodeTypes.IF,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'orNot',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 1,
},
},
},
{
type: IRNodeTypes.PREPEND_NODE,
elements: [1],
parent: 0,
},
])
})
test('v-if + v-else-if + v-else', () => {
const { code, ir } = compileWithVIf(
`<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(4)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: 'fine',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{ type: IRNodeTypes.FRAGMENT_FACTORY },
])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
negative: {
type: IRNodeTypes.IF,
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 1,
},
negative: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 2,
},
},
},
{
type: IRNodeTypes.PREPEND_NODE,
elements: [1],
parent: 0,
},
])
})
test('comment between branches', () => {
const { code, ir } = compileWithVIf(`
<div v-if="ok"/>
<!--foo-->
<p v-else-if="orNot"/>
<!--bar-->
<template v-else>fine</template>
`)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(4)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<!--foo--><p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<!--bar-->fine',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{ type: IRNodeTypes.FRAGMENT_FACTORY },
])
})
describe.todo('errors') describe.todo('errors')
describe.todo('codegen') describe.todo('codegen')
test.todo('v-on with v-if') test.todo('v-on with v-if')

View File

@ -1,12 +1,30 @@
import { type CodegenContext, genBlockFunctionContent } from '../generate' import { type CodegenContext, genBlockFunctionContent } from '../generate'
import type { BlockFunctionIRNode, IfIRNode } from '../ir' import { type BlockFunctionIRNode, IRNodeTypes, type IfIRNode } from '../ir'
import { genExpression } from './expression' import { genExpression } from './expression'
export function genIf(oper: IfIRNode, context: CodegenContext) { export function genIf(
const { pushFnCall, vaporHelper, pushNewline, push, withIndent } = context oper: IfIRNode,
context: CodegenContext,
isNested = false,
) {
const { pushFnCall, vaporHelper, pushNewline, push } = context
const { condition, positive, negative } = oper const { condition, positive, negative } = oper
pushNewline(`const n${oper.id} = `) let positiveArg = () => genBlockFunction(positive, context)
let negativeArg: false | (() => void) = false
if (negative) {
if (negative.type === IRNodeTypes.BLOCK_FUNCTION) {
negativeArg = () => genBlockFunction(negative, context)
} else {
negativeArg = () => {
push('() => ')
genIf(negative!, context, true)
}
}
}
if (!isNested) pushNewline(`const n${oper.id} = `)
pushFnCall( pushFnCall(
vaporHelper('createIf'), vaporHelper('createIf'),
() => { () => {
@ -14,15 +32,17 @@ export function genIf(oper: IfIRNode, context: CodegenContext) {
genExpression(condition, context) genExpression(condition, context)
push(')') push(')')
}, },
() => genBlockFunction(positive), positiveArg,
!!negative && (() => genBlockFunction(negative!)), negativeArg,
) )
}
function genBlockFunction(oper: BlockFunctionIRNode, context: CodegenContext) {
const { pushNewline, push, withIndent } = context
function genBlockFunction(oper: BlockFunctionIRNode) {
push('() => {') push('() => {')
withIndent(() => { withIndent(() => {
genBlockFunctionContent(oper, context) genBlockFunctionContent(oper, context)
}) })
pushNewline('}') pushNewline('}')
} }
}

View File

@ -62,7 +62,7 @@ export interface IfIRNode extends BaseIRNode {
id: number id: number
condition: IRExpression condition: IRExpression
positive: BlockFunctionIRNode positive: BlockFunctionIRNode
negative?: BlockFunctionIRNode negative?: BlockFunctionIRNode | IfIRNode
} }
export interface TemplateFactoryIRNode extends BaseIRNode { export interface TemplateFactoryIRNode extends BaseIRNode {

View File

@ -42,9 +42,9 @@ export type DirectiveTransform = (
// A structural directive transform is technically also a NodeTransform; // A structural directive transform is technically also a NodeTransform;
// Only v-if and v-for fall into this category. // Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = ( export type StructuralDirectiveTransform = (
node: RootNode | TemplateChildNode, node: ElementNode,
dir: VaporDirectiveNode, dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>, context: TransformContext<ElementNode>,
) => void | (() => void) ) => void | (() => void)
export type TransformOptions = HackOptions<BaseTransformOptions> export type TransformOptions = HackOptions<BaseTransformOptions>
@ -60,7 +60,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
> >
template: string template: string
childrenTemplate: string[] childrenTemplate: (string | null)[]
dynamic: IRDynamicInfo dynamic: IRDynamicInfo
inVOnce: boolean inVOnce: boolean
@ -311,15 +311,12 @@ function transformNode(
} }
if (context.node.type === NodeTypes.ROOT) if (context.node.type === NodeTypes.ROOT)
context.template += context.childrenTemplate.join('') context.template += context.childrenTemplate.filter(Boolean).join('')
} }
function transformChildren(ctx: TransformContext<RootNode | ElementNode>) { function transformChildren(ctx: TransformContext<RootNode | ElementNode>) {
const { children } = ctx.node const { children } = ctx.node
let i = 0 let i = 0
// const nodeRemoved = () => {
// i--
// }
for (; i < children.length; i++) { for (; i < children.length; i++) {
const child = children[i] const child = children[i]
const childContext = createContext(child, ctx, i) const childContext = createContext(child, ctx, i)
@ -405,7 +402,11 @@ export function createStructuralDirectiveTransform(
const exitFns = [] const exitFns = []
for (const prop of props) { for (const prop of props) {
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) { if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
const onExit = fn(node, prop as VaporDirectiveNode, context) const onExit = fn(
node,
prop as VaporDirectiveNode,
context as TransformContext<ElementNode>,
)
if (onExit) exitFns.push(onExit) if (onExit) exitFns.push(onExit)
} }
} }

View File

@ -1,4 +1,5 @@
import { import {
type ElementNode,
ElementTypes, ElementTypes,
ErrorCodes, ErrorCodes,
NodeTypes, NodeTypes,
@ -15,6 +16,7 @@ import {
import { import {
type BlockFunctionIRNode, type BlockFunctionIRNode,
IRNodeTypes, IRNodeTypes,
type OperationNode,
type VaporDirectiveNode, type VaporDirectiveNode,
} from '../ir' } from '../ir'
import { extend } from '@vue/shared' import { extend } from '@vue/shared'
@ -25,7 +27,7 @@ export const transformVIf = createStructuralDirectiveTransform(
) )
export function processIf( export function processIf(
node: RootNode | TemplateChildNode, node: ElementNode,
dir: VaporDirectiveNode, dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>, context: TransformContext<RootNode | TemplateChildNode>,
) { ) {
@ -40,7 +42,7 @@ export function processIf(
if (dir.name === 'if') { if (dir.name === 'if') {
const id = context.reference() const id = context.reference()
context.dynamic.ghost = true context.dynamic.ghost = true
const [branch, onExit] = createIfBranch(node, dir, context) const [branch, onExit] = createIfBranch(node, context)
return () => { return () => {
onExit() onExit()
@ -52,37 +54,100 @@ export function processIf(
positive: branch, positive: branch,
}) })
} }
} else {
// check the adjacent v-if
const parent = context.parent!
const siblings = parent.node.children
const templates = parent.childrenTemplate
const comments = []
let sibling: TemplateChildNode | undefined
let i = siblings.indexOf(node)
while (i-- >= -1) {
sibling = siblings[i]
if (sibling) {
if (sibling.type === NodeTypes.COMMENT) {
__DEV__ && comments.unshift(sibling)
templates[i] = null
continue
} else if (
sibling.type === NodeTypes.TEXT &&
!sibling.content.trim().length
) {
templates[i] = null
continue
}
}
break
}
const { operation } = context.block
let lastIfNode: OperationNode
if (
// check if v-if is the sibling node
!sibling ||
sibling.type !== NodeTypes.ELEMENT ||
!sibling.props.some(
({ type, name }) =>
type === NodeTypes.DIRECTIVE && ['if', 'else-if'].includes(name),
) ||
// check if IFNode is the last operation and get the root IFNode
!(lastIfNode = operation[operation.length - 1]) ||
lastIfNode.type !== IRNodeTypes.IF
) {
context.options.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
)
return
}
while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
lastIfNode = lastIfNode.negative
}
// Check if v-else was followed by v-else-if
if (dir.name === 'else-if' && lastIfNode.negative) {
context.options.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
)
}
// TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
if (__DEV__ && comments.length) {
node = wrapTemplate(node)
context.node = node = extend({}, node, {
children: [...comments, ...node.children],
})
}
const [branch, onExit] = createIfBranch(node, context)
if (dir.name === 'else') {
lastIfNode.negative = branch
} else {
lastIfNode.negative = {
type: IRNodeTypes.IF,
id: -1,
loc: dir.loc,
condition: dir.exp!,
positive: branch,
}
}
return () => onExit()
} }
} }
export function createIfBranch( export function createIfBranch(
node: RootNode | TemplateChildNode, node: ElementNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>, context: TransformContext<RootNode | TemplateChildNode>,
): [BlockFunctionIRNode, () => void] { ): [BlockFunctionIRNode, () => void] {
if ( context.node = node = wrapTemplate(node)
node.type === NodeTypes.ELEMENT &&
node.tagType !== ElementTypes.TEMPLATE
) {
node = extend({}, node, {
type: NodeTypes.ELEMENT,
tag: 'template',
props: [],
tagType: ElementTypes.TEMPLATE,
children: [
extend({}, node, {
props: node.props.filter(
p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
),
} as TemplateChildNode),
],
} as Partial<TemplateNode>)
context.node = node
}
const branch: BlockFunctionIRNode = { const branch: BlockFunctionIRNode = {
type: IRNodeTypes.BLOCK_FUNCTION, type: IRNodeTypes.BLOCK_FUNCTION,
loc: dir.loc, loc: node.loc,
node, node,
templateIndex: -1, templateIndex: -1,
dynamic: { dynamic: {
@ -105,3 +170,22 @@ export function createIfBranch(
} }
return [branch, onExit] return [branch, onExit]
} }
function wrapTemplate(node: ElementNode): TemplateNode {
if (node.tagType === ElementTypes.TEMPLATE) {
return node
}
return extend({}, node, {
type: NodeTypes.ELEMENT,
tag: 'template',
props: [],
tagType: ElementTypes.TEMPLATE,
children: [
extend({}, node, {
props: node.props.filter(
p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
),
} as TemplateChildNode),
],
} as Partial<TemplateNode>)
}