feat(compiler/runtime-vapor): implement v-slots + v-for / v-if (#207)

Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Doctor Wu 2024-05-21 08:50:10 +08:00 committed by GitHub
parent 2e2f3e2b96
commit 4e13a57d9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 434 additions and 53 deletions

View File

@ -17,6 +17,78 @@ export function render(_ctx) {
}"
`;
exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createForSlots as _createForSlots, template as _template } from 'vue/vapor';
const t0 = _template("foo")
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n2 = _createComponent(_component_Comp, null, null, () => [_createForSlots(_ctx.list, (item) => ({
name: item,
fn: () => {
const n0 = t0()
return n0
}
}))], true)
return n2
}"
`;
exports[`compiler: transform slot > dynamic slots name w/ v-for and provide absent key 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createForSlots as _createForSlots, template as _template } from 'vue/vapor';
const t0 = _template("foo")
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n2 = _createComponent(_component_Comp, null, null, () => [_createForSlots(_ctx.list, (_, __, index) => ({
name: index,
fn: () => {
const n0 = t0()
return n0
}
}))], true)
return n2
}"
`;
exports[`compiler: transform slot > dynamic slots name w/ v-if / v-else[-if] 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor';
const t0 = _template("condition slot")
const t1 = _template("another condition")
const t2 = _template("else condition")
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n6 = _createComponent(_component_Comp, null, null, () => [_ctx.condition
? {
name: "condition",
fn: () => {
const n0 = t0()
return n0
},
key: "0"
}
: _ctx.anotherCondition
? {
name: "condition",
fn: () => {
const n2 = t1()
return n2
},
key: "1"
}
: {
name: "condition",
fn: () => {
const n4 = t2()
return n4
},
key: "2"
}], true)
return n6
}"
`;
exports[`compiler: transform slot > implicit default slot 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")

View File

@ -1,5 +1,6 @@
import { ErrorCodes, NodeTypes } from '@vue/compiler-core'
import {
DynamicSlotType,
IRNodeTypes,
transformChildren,
transformElement,
@ -126,6 +127,112 @@ describe('compiler: transform slot', () => {
])
})
test('dynamic slots name w/ v-for', () => {
const { ir, code } = compileWithSlots(
`<Comp>
<template v-for="item in list" #[item]>foo</template>
</Comp>`,
)
expect(code).toMatchSnapshot()
expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE)
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.CREATE_COMPONENT_NODE,
tag: 'Comp',
slots: undefined,
dynamicSlots: [
{
name: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'item',
isStatic: false,
},
fn: { type: IRNodeTypes.BLOCK },
loop: {
source: { content: 'list' },
value: { content: 'item' },
key: undefined,
index: undefined,
},
},
],
},
])
})
test('dynamic slots name w/ v-for and provide absent key', () => {
const { ir, code } = compileWithSlots(
`<Comp>
<template v-for="(,,index) in list" #[index]>foo</template>
</Comp>`,
)
expect(code).toMatchSnapshot()
expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE)
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.CREATE_COMPONENT_NODE,
tag: 'Comp',
slots: undefined,
dynamicSlots: [
{
name: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'index',
isStatic: false,
},
fn: { type: IRNodeTypes.BLOCK },
loop: {
source: { content: 'list' },
value: undefined,
key: undefined,
index: {
type: NodeTypes.SIMPLE_EXPRESSION,
},
},
},
],
},
])
})
test('dynamic slots name w/ v-if / v-else[-if]', () => {
const { ir, code } = compileWithSlots(
`<Comp>
<template v-if="condition" #condition>condition slot</template>
<template v-else-if="anotherCondition" #condition>another condition</template>
<template v-else #condition>else condition</template>
</Comp>`,
)
expect(code).toMatchSnapshot()
expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE)
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.CREATE_COMPONENT_NODE,
tag: 'Comp',
slots: undefined,
dynamicSlots: [
{
slotType: DynamicSlotType.CONDITIONAL,
condition: { content: 'condition' },
positive: {
slotType: DynamicSlotType.BASIC,
key: 0,
},
negative: {
slotType: DynamicSlotType.CONDITIONAL,
condition: { content: 'anotherCondition' },
positive: {
slotType: DynamicSlotType.BASIC,
key: 1,
},
negative: { slotType: DynamicSlotType.BASIC, key: 2 },
},
},
],
},
])
})
describe('errors', () => {
test('error on extraneous children w/ named default slot', () => {
const onError = vi.fn()

View File

@ -1,9 +1,13 @@
import { camelize, extend, isArray } from '@vue/shared'
import type { CodegenContext } from '../generate'
import {
type ComponentBasicDynamicSlot,
type ComponentConditionalDynamicSlot,
type ComponentDynamicSlot,
type ComponentLoopDynamicSlot,
type ComponentSlots,
type CreateComponentIRNode,
DynamicSlotType,
IRDynamicPropsKind,
type IRProp,
type IRProps,
@ -15,6 +19,8 @@ import {
DELIMITERS_ARRAY_NEWLINE,
DELIMITERS_OBJECT,
DELIMITERS_OBJECT_NEWLINE,
INDENT_END,
INDENT_START,
NEWLINE,
genCall,
genMulti,
@ -155,13 +161,90 @@ function genDynamicSlots(
) {
const slotsExpr = genMulti(
dynamicSlots.length > 1 ? DELIMITERS_ARRAY_NEWLINE : DELIMITERS_ARRAY,
...dynamicSlots.map(({ name, fn }) =>
genMulti(
DELIMITERS_OBJECT_NEWLINE,
['name: ', ...genExpression(name, context)],
['fn: ', ...genBlock(fn, context)],
),
),
...dynamicSlots.map(slot => genDynamicSlot(slot, context)),
)
return ['() => ', ...slotsExpr]
}
function genDynamicSlot(
slot: ComponentDynamicSlot,
context: CodegenContext,
): CodeFragment[] {
switch (slot.slotType) {
case DynamicSlotType.BASIC:
return genBasicDynamicSlot(slot, context)
case DynamicSlotType.LOOP:
return genLoopSlot(slot, context)
case DynamicSlotType.CONDITIONAL:
return genConditionalSlot(slot, context)
}
}
function genBasicDynamicSlot(
slot: ComponentBasicDynamicSlot,
context: CodegenContext,
): CodeFragment[] {
const { name, fn, key } = slot
return genMulti(
DELIMITERS_OBJECT_NEWLINE,
['name: ', ...genExpression(name, context)],
['fn: ', ...genBlock(fn, context)],
...(key !== undefined ? [`key: "${key}"`] : []),
)
}
function genLoopSlot(
slot: ComponentLoopDynamicSlot,
context: CodegenContext,
): CodeFragment[] {
const { name, fn, loop } = slot
const { value, key, index, source } = loop
const rawValue = value && value.content
const rawKey = key && key.content
const rawIndex = index && index.content
const idMap: Record<string, string> = {}
if (rawValue) idMap[rawValue] = rawValue
if (rawKey) idMap[rawKey] = rawKey
if (rawIndex) idMap[rawIndex] = rawIndex
const slotExpr = genMulti(
DELIMITERS_OBJECT_NEWLINE,
['name: ', ...context.withId(() => genExpression(name, context), idMap)],
['fn: ', ...context.withId(() => genBlock(fn, context), idMap)],
)
return [
...genCall(
context.vaporHelper('createForSlots'),
genExpression(source, context),
[
...genMulti(
['(', ')', ', '],
rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
rawKey ? rawKey : rawIndex ? '__' : undefined,
rawIndex,
),
' => (',
...slotExpr,
')',
],
),
]
}
function genConditionalSlot(
slot: ComponentConditionalDynamicSlot,
context: CodegenContext,
): CodeFragment[] {
const { condition, positive, negative } = slot
return [
...genExpression(condition, context),
INDENT_START,
NEWLINE,
'? ',
...genDynamicSlot(positive, context),
NEWLINE,
': ',
...(negative ? [...genDynamicSlot(negative, context)] : ['void 0']),
INDENT_END,
]
}

View File

@ -73,13 +73,16 @@ export interface IfIRNode extends BaseIRNode {
once?: boolean
}
export interface ForIRNode extends BaseIRNode {
type: IRNodeTypes.FOR
id: number
export interface IRFor {
source: SimpleExpressionNode
value?: SimpleExpressionNode
key?: SimpleExpressionNode
index?: SimpleExpressionNode
}
export interface ForIRNode extends BaseIRNode, IRFor {
type: IRNodeTypes.FOR
id: number
keyProp?: SimpleExpressionNode
render: BlockIRNode
once: boolean
@ -208,12 +211,39 @@ export interface ComponentSlotBlockIRNode extends BlockIRNode {
// TODO slot props
}
export type ComponentSlots = Record<string, ComponentSlotBlockIRNode>
export interface ComponentDynamicSlot {
export enum DynamicSlotType {
BASIC,
LOOP,
CONDITIONAL,
}
export interface ComponentBasicDynamicSlot {
slotType: DynamicSlotType.BASIC
name: SimpleExpressionNode
fn: ComponentSlotBlockIRNode
key?: string
key?: number
}
export interface ComponentLoopDynamicSlot {
slotType: DynamicSlotType.LOOP
name: SimpleExpressionNode
fn: ComponentSlotBlockIRNode
loop: IRFor
}
export interface ComponentConditionalDynamicSlot {
slotType: DynamicSlotType.CONDITIONAL
condition: SimpleExpressionNode
positive: ComponentBasicDynamicSlot
negative?: ComponentBasicDynamicSlot | ComponentConditionalDynamicSlot
}
export type ComponentDynamicSlot =
| ComponentBasicDynamicSlot
| ComponentLoopDynamicSlot
| ComponentConditionalDynamicSlot
export interface CreateComponentIRNode extends BaseIRNode {
type: IRNodeTypes.CREATE_COMPONENT_NODE
id: number

View File

@ -10,7 +10,15 @@ import {
} from '@vue/compiler-core'
import type { NodeTransform, TransformContext } from '../transform'
import { newBlock } from './utils'
import { type BlockIRNode, DynamicFlag, type VaporDirectiveNode } from '../ir'
import {
type BlockIRNode,
type ComponentBasicDynamicSlot,
type ComponentConditionalDynamicSlot,
DynamicFlag,
DynamicSlotType,
type IRFor,
type VaporDirectiveNode,
} from '../ir'
import { findDir, resolveExpression } from '../utils'
// TODO dynamic slots
@ -69,6 +77,9 @@ export const transformVSlot: NodeTransform = (node, context) => {
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
const vFor = findDir(node, 'for')
const vIf = findDir(node, 'if')
const vElse = findDir(node, /^else(-if)?$/, true /* allowEmpty */)
const slots = context.slots!
const dynamicSlots = context.dynamicSlots!
@ -79,7 +90,7 @@ export const transformVSlot: NodeTransform = (node, context) => {
arg &&= resolveExpression(arg)
if (!arg || arg.isStatic) {
if ((!arg || arg.isStatic) && !vFor && !vIf && !vElse) {
const slotName = arg ? arg.content : 'default'
if (slots[slotName]) {
@ -92,12 +103,75 @@ export const transformVSlot: NodeTransform = (node, context) => {
} else {
slots[slotName] = block
}
} else if (vIf) {
dynamicSlots.push({
slotType: DynamicSlotType.CONDITIONAL,
condition: vIf.exp!,
positive: {
slotType: DynamicSlotType.BASIC,
name: arg!,
fn: block,
key: 0,
},
})
} else if (vElse) {
const vIfIR = dynamicSlots[dynamicSlots.length - 1]
if (vIfIR.slotType === DynamicSlotType.CONDITIONAL) {
let ifNode = vIfIR
while (
ifNode.negative &&
ifNode.negative.slotType === DynamicSlotType.CONDITIONAL
)
ifNode = ifNode.negative
const negative:
| ComponentBasicDynamicSlot
| ComponentConditionalDynamicSlot = vElse.exp
? {
slotType: DynamicSlotType.CONDITIONAL,
condition: vElse.exp,
positive: {
slotType: DynamicSlotType.BASIC,
name: arg!,
fn: block,
key: ifNode.positive.key! + 1,
},
}
: {
slotType: DynamicSlotType.BASIC,
name: arg!,
fn: block,
key: ifNode.positive.key! + 1,
}
ifNode.negative = negative
} else {
context.options.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc),
)
}
} else if (vFor) {
if (vFor.forParseResult) {
dynamicSlots.push({
slotType: DynamicSlotType.LOOP,
name: arg!,
fn: block,
loop: vFor.forParseResult as IRFor,
})
} else {
context.options.onError(
createCompilerError(
ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
vFor.loc,
),
)
}
} else {
dynamicSlots.push({
name: arg,
slotType: DynamicSlotType.BASIC,
name: arg!,
fn: block,
})
}
return () => onExit()
}
}

View File

@ -5,6 +5,7 @@ import { renderEffect } from './renderEffect'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { warn } from './warning'
import { componentKey } from './component'
import type { DynamicSlot } from './componentSlots'
interface ForBlock extends Fragment {
scope: EffectScope
@ -301,44 +302,57 @@ export const createFor = (
remove(nodes, parent!)
scope.stop()
}
}
function getLength(source: any): number {
if (isArray(source) || isString(source)) {
return source.length
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {
warn(`The v-for range expect an integer value but got ${source}.`)
}
return source
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
return Array.from(source as Iterable<any>).length
} else {
return Object.keys(source).length
}
}
return 0
export function createForSlots(
source: any[] | Record<any, any> | number | Set<any> | Map<any, any>,
getSlot: (item: any, key: any, index?: number) => DynamicSlot,
): DynamicSlot[] {
const sourceLength = getLength(source)
const slots = new Array<DynamicSlot>(sourceLength)
for (let i = 0; i < sourceLength; i++) {
const [item, key, index] = getItem(source, i)
slots[i] = getSlot(item, key, index)
}
return slots
}
function getItem(
source: any,
idx: number,
): [item: any, key: any, index?: number] {
if (isArray(source) || isString(source)) {
function getLength(source: any): number {
if (isArray(source) || isString(source)) {
return source.length
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {
warn(`The v-for range expect an integer value but got ${source}.`)
}
return source
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
return Array.from(source as Iterable<any>).length
} else {
return Object.keys(source).length
}
}
return 0
}
function getItem(
source: any,
idx: number,
): [item: any, key: any, index?: number] {
if (isArray(source) || isString(source)) {
return [source[idx], idx, undefined]
} else if (typeof source === 'number') {
return [idx + 1, idx, undefined]
} else if (isObject(source)) {
if (source && source[Symbol.iterator as any]) {
source = Array.from(source as Iterable<any>)
return [source[idx], idx, undefined]
} else if (typeof source === 'number') {
return [idx + 1, idx, undefined]
} else if (isObject(source)) {
if (source && source[Symbol.iterator as any]) {
source = Array.from(source as Iterable<any>)
return [source[idx], idx, undefined]
} else {
const key = Object.keys(source)[idx]
return [source[key], key, idx]
}
} else {
const key = Object.keys(source)[idx]
return [source[key], key, idx]
}
return null!
}
return null!
}
function normalizeAnchor(node: Block): Node {

View File

@ -53,11 +53,12 @@ export function initSlots(
slots = shallowReactive(slots)
const dynamicSlotKeys: Record<string, true> = {}
firstEffect(instance, () => {
const _dynamicSlots = callWithAsyncErrorHandling(
dynamicSlots,
instance,
VaporErrorCodes.RENDER_FUNCTION,
)
const _dynamicSlots: (DynamicSlot | DynamicSlot[])[] =
callWithAsyncErrorHandling(
dynamicSlots,
instance,
VaporErrorCodes.RENDER_FUNCTION,
)
for (let i = 0; i < _dynamicSlots.length; i++) {
const slot = _dynamicSlots[i]
// array of dynamic slot generated by <template v-for="..." #[...]>

View File

@ -126,7 +126,7 @@ export {
type FunctionPlugin,
} from './apiCreateVaporApp'
export { createIf } from './apiCreateIf'
export { createFor } from './apiCreateFor'
export { createFor, createForSlots } from './apiCreateFor'
export { createComponent } from './apiCreateComponent'
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'