feat(compiler): optimize text by merging adjacent nodes
This commit is contained in:
@@ -44,7 +44,7 @@ exports[`compiler: codegen compound expression 1`] = `
|
||||
"
|
||||
return function render() {
|
||||
with (this) {
|
||||
return _toString(_ctx.foo)
|
||||
return _ctx.foo + _toString(bar)
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -226,17 +226,23 @@ describe('compiler: codegen', () => {
|
||||
const { code } = generate(
|
||||
createRoot({
|
||||
children: [
|
||||
createInterpolation(
|
||||
createCompoundExpression(
|
||||
[`_ctx.`, createSimpleExpression(`foo`, false, mockLoc)],
|
||||
mockLoc
|
||||
),
|
||||
createCompoundExpression(
|
||||
[
|
||||
`_ctx.`,
|
||||
createSimpleExpression(`foo`, false, mockLoc),
|
||||
` + `,
|
||||
{
|
||||
type: NodeTypes.INTERPOLATION,
|
||||
loc: mockLoc,
|
||||
content: createSimpleExpression(`bar`, false, mockLoc)
|
||||
}
|
||||
],
|
||||
mockLoc
|
||||
)
|
||||
]
|
||||
})
|
||||
)
|
||||
expect(code).toMatch(`return _${TO_STRING}(_ctx.foo)`)
|
||||
expect(code).toMatch(`return _ctx.foo + _${TO_STRING}(bar)`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
|
||||
@@ -25,22 +25,29 @@ describe('compiler: transform', () => {
|
||||
})
|
||||
|
||||
const div = ast.children[0] as ElementNode
|
||||
expect(calls.length).toBe(3)
|
||||
expect(calls.length).toBe(4)
|
||||
expect(calls[0]).toMatchObject([
|
||||
ast,
|
||||
{
|
||||
parent: null,
|
||||
currentNode: ast
|
||||
}
|
||||
])
|
||||
expect(calls[1]).toMatchObject([
|
||||
div,
|
||||
{
|
||||
parent: ast,
|
||||
currentNode: div
|
||||
}
|
||||
])
|
||||
expect(calls[1]).toMatchObject([
|
||||
expect(calls[2]).toMatchObject([
|
||||
div.children[0],
|
||||
{
|
||||
parent: div,
|
||||
currentNode: div.children[0]
|
||||
}
|
||||
])
|
||||
expect(calls[2]).toMatchObject([
|
||||
expect(calls[3]).toMatchObject([
|
||||
div.children[1],
|
||||
{
|
||||
parent: div,
|
||||
@@ -76,11 +83,11 @@ describe('compiler: transform', () => {
|
||||
expect(ast.children.length).toBe(2)
|
||||
const newElement = ast.children[0] as ElementNode
|
||||
expect(newElement.tag).toBe('p')
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
expect(spy).toHaveBeenCalledTimes(4)
|
||||
// should traverse the children of replaced node
|
||||
expect(spy.mock.calls[1][0]).toBe(newElement.children[0])
|
||||
expect(spy.mock.calls[2][0]).toBe(newElement.children[0])
|
||||
// should traverse the node after the replaced node
|
||||
expect(spy.mock.calls[2][0]).toBe(ast.children[1])
|
||||
expect(spy.mock.calls[3][0]).toBe(ast.children[1])
|
||||
})
|
||||
|
||||
test('context.removeNode', () => {
|
||||
@@ -103,10 +110,10 @@ describe('compiler: transform', () => {
|
||||
expect(ast.children[1]).toBe(c2)
|
||||
|
||||
// should not traverse children of remove node
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
expect(spy).toHaveBeenCalledTimes(4)
|
||||
// should traverse nodes around removed
|
||||
expect(spy.mock.calls[0][0]).toBe(c1)
|
||||
expect(spy.mock.calls[2][0]).toBe(c2)
|
||||
expect(spy.mock.calls[1][0]).toBe(c1)
|
||||
expect(spy.mock.calls[3][0]).toBe(c2)
|
||||
})
|
||||
|
||||
test('context.removeNode (prev sibling)', () => {
|
||||
@@ -118,7 +125,7 @@ describe('compiler: transform', () => {
|
||||
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
|
||||
context.removeNode()
|
||||
// remove previous sibling
|
||||
context.removeNode(context.parent.children[0])
|
||||
context.removeNode(context.parent!.children[0])
|
||||
}
|
||||
}
|
||||
const spy = jest.fn(plugin)
|
||||
@@ -129,11 +136,11 @@ describe('compiler: transform', () => {
|
||||
expect(ast.children.length).toBe(1)
|
||||
expect(ast.children[0]).toBe(c2)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
expect(spy).toHaveBeenCalledTimes(4)
|
||||
// should still traverse first span before removal
|
||||
expect(spy.mock.calls[0][0]).toBe(c1)
|
||||
expect(spy.mock.calls[1][0]).toBe(c1)
|
||||
// should still traverse last span
|
||||
expect(spy.mock.calls[2][0]).toBe(c2)
|
||||
expect(spy.mock.calls[3][0]).toBe(c2)
|
||||
})
|
||||
|
||||
test('context.removeNode (next sibling)', () => {
|
||||
@@ -145,7 +152,7 @@ describe('compiler: transform', () => {
|
||||
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
|
||||
context.removeNode()
|
||||
// remove next sibling
|
||||
context.removeNode(context.parent.children[1])
|
||||
context.removeNode(context.parent!.children[1])
|
||||
}
|
||||
}
|
||||
const spy = jest.fn(plugin)
|
||||
@@ -156,20 +163,22 @@ describe('compiler: transform', () => {
|
||||
expect(ast.children.length).toBe(1)
|
||||
expect(ast.children[0]).toBe(c1)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
// should still traverse first span before removal
|
||||
expect(spy.mock.calls[0][0]).toBe(c1)
|
||||
expect(spy.mock.calls[1][0]).toBe(c1)
|
||||
// should not traverse last span
|
||||
expect(spy.mock.calls[1][0]).toBe(d1)
|
||||
expect(spy.mock.calls[2][0]).toBe(d1)
|
||||
})
|
||||
|
||||
test('context.hoist', () => {
|
||||
const ast = parse(`<div :id="foo"/><div :id="bar"/>`)
|
||||
const hoisted: ExpressionNode[] = []
|
||||
const mock: NodeTransform = (node, context) => {
|
||||
const dir = (node as ElementNode).props[0] as DirectiveNode
|
||||
hoisted.push(dir.exp!)
|
||||
dir.exp = context.hoist(dir.exp!)
|
||||
if (node.type === NodeTypes.ELEMENT) {
|
||||
const dir = node.props[0] as DirectiveNode
|
||||
hoisted.push(dir.exp!)
|
||||
dir.exp = context.hoist(dir.exp!)
|
||||
}
|
||||
}
|
||||
transform(ast, {
|
||||
nodeTransforms: [mock]
|
||||
|
||||
112
packages/compiler-core/__tests__/transforms/optimizeText.spec.ts
Normal file
112
packages/compiler-core/__tests__/transforms/optimizeText.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { CompilerOptions, parse, transform, NodeTypes } from '../../src'
|
||||
import { optimizeText } from '../../src/transforms/optimizeText'
|
||||
import { transformExpression } from '../../src/transforms/transformExpression'
|
||||
|
||||
function transformWithTextOpt(template: string, options: CompilerOptions = {}) {
|
||||
const ast = parse(template)
|
||||
transform(ast, {
|
||||
nodeTransforms: [
|
||||
...(options.prefixIdentifiers ? [transformExpression] : []),
|
||||
optimizeText
|
||||
],
|
||||
...options
|
||||
})
|
||||
return ast
|
||||
}
|
||||
|
||||
describe('compiler: optimize interpolation', () => {
|
||||
test('no consecutive text', () => {
|
||||
const root = transformWithTextOpt(`{{ foo }}`)
|
||||
expect(root.children[0]).toMatchObject({
|
||||
type: NodeTypes.INTERPOLATION,
|
||||
content: {
|
||||
content: `foo`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('consecutive text', () => {
|
||||
const root = transformWithTextOpt(`{{ foo }} bar {{ baz }}`)
|
||||
expect(root.children.length).toBe(1)
|
||||
expect(root.children[0]).toMatchObject({
|
||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||
children: [
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
|
||||
` + `,
|
||||
{ type: NodeTypes.TEXT, content: ` bar ` },
|
||||
` + `,
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('consecutive text between elements', () => {
|
||||
const root = transformWithTextOpt(`<div/>{{ foo }} bar {{ baz }}<div/>`)
|
||||
expect(root.children.length).toBe(3)
|
||||
expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
|
||||
expect(root.children[1]).toMatchObject({
|
||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||
children: [
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
|
||||
` + `,
|
||||
{ type: NodeTypes.TEXT, content: ` bar ` },
|
||||
` + `,
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
|
||||
]
|
||||
})
|
||||
expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
|
||||
})
|
||||
|
||||
test('consecutive text mixed with elements', () => {
|
||||
const root = transformWithTextOpt(
|
||||
`<div/>{{ foo }} bar {{ baz }}<div/>{{ foo }} bar {{ baz }}<div/>`
|
||||
)
|
||||
expect(root.children.length).toBe(5)
|
||||
expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
|
||||
expect(root.children[1]).toMatchObject({
|
||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||
children: [
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
|
||||
` + `,
|
||||
{ type: NodeTypes.TEXT, content: ` bar ` },
|
||||
` + `,
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
|
||||
]
|
||||
})
|
||||
expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
|
||||
expect(root.children[3]).toMatchObject({
|
||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||
children: [
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
|
||||
` + `,
|
||||
{ type: NodeTypes.TEXT, content: ` bar ` },
|
||||
` + `,
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
|
||||
]
|
||||
})
|
||||
expect(root.children[4].type).toBe(NodeTypes.ELEMENT)
|
||||
})
|
||||
|
||||
test('with prefixIdentifiers: true', () => {
|
||||
const root = transformWithTextOpt(`{{ foo }} bar {{ baz + qux }}`, {
|
||||
prefixIdentifiers: true
|
||||
})
|
||||
expect(root.children.length).toBe(1)
|
||||
expect(root.children[0]).toMatchObject({
|
||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||
children: [
|
||||
{ type: NodeTypes.INTERPOLATION, content: { content: `_ctx.foo` } },
|
||||
` + `,
|
||||
{ type: NodeTypes.TEXT, content: ` bar ` },
|
||||
` + `,
|
||||
{
|
||||
type: NodeTypes.INTERPOLATION,
|
||||
content: {
|
||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||
children: [{ content: `_ctx.baz` }, ` + `, { content: `_ctx.qux` }]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user