feat(compiler): convert text mixed with elements into createVNode calls

This ensures they are tracked as dynamic children when inside blocks.
Also guaruntees compiled vnodes always have vnode children in arrays
so that they can skip normalizeVNode safely in optimized mode.
This commit is contained in:
Evan You 2019-10-21 15:52:29 -04:00
parent a0d570b16d
commit 052febc127
13 changed files with 236 additions and 112 deletions

View File

@ -5,13 +5,13 @@ exports[`compiler: integration tests function mode 1`] = `
return function render() { return function render() {
with (this) { with (this) {
const { toString: _toString, openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, Comment: _Comment, Fragment: _Fragment, renderList: _renderList } = _Vue const { toString: _toString, openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, Comment: _Comment, Fragment: _Fragment, renderList: _renderList, Text: _Text } = _Vue
return (_openBlock(), _createBlock(\\"div\\", { return (_openBlock(), _createBlock(\\"div\\", {
id: \\"foo\\", id: \\"foo\\",
class: bar.baz class: bar.baz
}, [ }, [
_toString(world.burn()), _createVNode(_Text, null, _toString(world.burn()), 1 /* TEXT */),
(_openBlock(), ok (_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? _createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: _createBlock(_Fragment, { key: 1 }, [\\"no\\"])), : _createBlock(_Fragment, { key: 1 }, [\\"no\\"])),
@ -26,7 +26,7 @@ return function render() {
`; `;
exports[`compiler: integration tests function mode w/ prefixIdentifiers: true 1`] = ` exports[`compiler: integration tests function mode w/ prefixIdentifiers: true 1`] = `
"const { toString, openBlock, createVNode, createBlock, Comment, Fragment, renderList } = Vue "const { toString, openBlock, createVNode, createBlock, Comment, Fragment, renderList, Text } = Vue
return function render() { return function render() {
const _ctx = this const _ctx = this
@ -34,7 +34,7 @@ return function render() {
id: \\"foo\\", id: \\"foo\\",
class: _ctx.bar.baz class: _ctx.bar.baz
}, [ }, [
toString(_ctx.world.burn()), createVNode(Text, null, toString(_ctx.world.burn()), 1 /* TEXT */),
(openBlock(), (_ctx.ok) (openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [\\"no\\"])), : createBlock(Fragment, { key: 1 }, [\\"no\\"])),
@ -48,7 +48,7 @@ return function render() {
`; `;
exports[`compiler: integration tests module mode 1`] = ` exports[`compiler: integration tests module mode 1`] = `
"import { toString, openBlock, createVNode, createBlock, Comment, Fragment, renderList } from \\"vue\\" "import { toString, openBlock, createVNode, createBlock, Comment, Fragment, renderList, Text } from \\"vue\\"
export default function render() { export default function render() {
const _ctx = this const _ctx = this
@ -56,7 +56,7 @@ export default function render() {
id: \\"foo\\", id: \\"foo\\",
class: _ctx.bar.baz class: _ctx.bar.baz
}, [ }, [
toString(_ctx.world.burn()), createVNode(Text, null, toString(_ctx.world.burn()), 1 /* TEXT */),
(openBlock(), (_ctx.ok) (openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [\\"no\\"])), : createBlock(Fragment, { key: 1 }, [\\"no\\"])),

View File

@ -21,7 +21,7 @@ import { transformIf } from '../src/transforms/vIf'
import { transformFor } from '../src/transforms/vFor' import { transformFor } from '../src/transforms/vFor'
import { transformElement } from '../src/transforms/transformElement' import { transformElement } from '../src/transforms/transformElement'
import { transformSlotOutlet } from '../src/transforms/transformSlotOutlet' import { transformSlotOutlet } from '../src/transforms/transformSlotOutlet'
import { optimizeText } from '../src/transforms/optimizeText' import { transformText } from '../src/transforms/transformText'
describe('compiler: transform', () => { describe('compiler: transform', () => {
test('context state', () => { test('context state', () => {
@ -243,7 +243,7 @@ describe('compiler: transform', () => {
nodeTransforms: [ nodeTransforms: [
transformIf, transformIf,
transformFor, transformFor,
optimizeText, transformText,
transformSlotOutlet, transformSlotOutlet,
transformElement transformElement
] ]

View File

@ -1,68 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: optimize interpolation consecutive text 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString } = _Vue
return _toString(foo) + \\" bar \\" + _toString(baz)
}
}"
`;
exports[`compiler: optimize interpolation consecutive text between elements 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, toString: _toString, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_toString(foo) + \\" bar \\" + _toString(baz),
_createVNode(\\"div\\")
]))
}
}"
`;
exports[`compiler: optimize interpolation consecutive text mixed with elements 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, toString: _toString, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_toString(foo) + \\" bar \\" + _toString(baz),
_createVNode(\\"div\\"),
_toString(foo) + \\" bar \\" + _toString(baz),
_createVNode(\\"div\\")
]))
}
}"
`;
exports[`compiler: optimize interpolation no consecutive text 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString } = _Vue
return _toString(foo)
}
}"
`;
exports[`compiler: optimize interpolation with prefixIdentifiers: true 1`] = `
"const { toString } = Vue
return function render() {
const _ctx = this
return toString(_ctx.foo) + \\" bar \\" + toString(_ctx.baz + _ctx.qux)
}"
`;

View File

@ -0,0 +1,84 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform text consecutive text 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString } = _Vue
return _toString(foo) + \\" bar \\" + _toString(baz)
}
}"
`;
exports[`compiler: transform text consecutive text between elements 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, toString: _toString, Text: _Text, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_createVNode(_Text, null, _toString(foo) + \\" bar \\" + _toString(baz), 1 /* TEXT */),
_createVNode(\\"div\\")
]))
}
}"
`;
exports[`compiler: transform text consecutive text mixed with elements 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, toString: _toString, Text: _Text, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_createVNode(_Text, null, _toString(foo) + \\" bar \\" + _toString(baz), 1 /* TEXT */),
_createVNode(\\"div\\"),
_createVNode(_Text, null, \\"hello\\"),
_createVNode(\\"div\\")
]))
}
}"
`;
exports[`compiler: transform text no consecutive text 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString } = _Vue
return _toString(foo)
}
}"
`;
exports[`compiler: transform text text between elements (static) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, Text: _Text, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_createVNode(_Text, null, \\"hello\\"),
_createVNode(\\"div\\")
]))
}
}"
`;
exports[`compiler: transform text with prefixIdentifiers: true 1`] = `
"const { toString } = Vue
return function render() {
const _ctx = this
return toString(_ctx.foo) + \\" bar \\" + toString(_ctx.baz + _ctx.qux)
}"
`;

View File

@ -23,7 +23,7 @@ import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils' import { createObjectMatcher, genFlagText } from '../testUtils'
import { optimizeText } from '../../src/transforms/optimizeText' import { transformText } from '../../src/transforms/transformText'
function parseWithElementTransform( function parseWithElementTransform(
template: string, template: string,
@ -36,7 +36,7 @@ function parseWithElementTransform(
// block as root node // block as root node
const ast = parse(`<div>${template}</div>`, options) const ast = parse(`<div>${template}</div>`, options)
transform(ast, { transform(ast, {
nodeTransforms: [transformElement, optimizeText], nodeTransforms: [transformElement, transformText],
...options ...options
}) })
const codegenNode = (ast as any).children[0].children[0] const codegenNode = (ast as any).children[0].children[0]

View File

@ -5,16 +5,19 @@ import {
NodeTypes, NodeTypes,
generate generate
} from '../../src' } from '../../src'
import { optimizeText } from '../../src/transforms/optimizeText' import { transformText } from '../../src/transforms/transformText'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { CREATE_VNODE, TEXT } from '../../src/runtimeHelpers'
import { genFlagText } from '../testUtils'
import { PatchFlags } from '@vue/shared'
function transformWithTextOpt(template: string, options: CompilerOptions = {}) { function transformWithTextOpt(template: string, options: CompilerOptions = {}) {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [ nodeTransforms: [
...(options.prefixIdentifiers ? [transformExpression] : []), ...(options.prefixIdentifiers ? [transformExpression] : []),
optimizeText, transformText,
transformElement transformElement
], ],
...options ...options
@ -22,7 +25,7 @@ function transformWithTextOpt(template: string, options: CompilerOptions = {}) {
return ast return ast
} }
describe('compiler: optimize interpolation', () => { describe('compiler: transform text', () => {
test('no consecutive text', () => { test('no consecutive text', () => {
const root = transformWithTextOpt(`{{ foo }}`) const root = transformWithTextOpt(`{{ foo }}`)
expect(root.children[0]).toMatchObject({ expect(root.children[0]).toMatchObject({
@ -55,6 +58,15 @@ describe('compiler: optimize interpolation', () => {
expect(root.children.length).toBe(3) expect(root.children.length).toBe(3)
expect(root.children[0].type).toBe(NodeTypes.ELEMENT) expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
expect(root.children[1]).toMatchObject({ expect(root.children[1]).toMatchObject({
// when mixed with elements, should convert it into a text node call
type: NodeTypes.TEXT_CALL,
codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
TEXT,
`null`,
{
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
{ type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, { type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
@ -63,6 +75,35 @@ describe('compiler: optimize interpolation', () => {
` + `, ` + `,
{ type: NodeTypes.INTERPOLATION, content: { content: `baz` } } { type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
] ]
},
genFlagText(PatchFlags.TEXT)
]
}
})
expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
expect(generate(root).code).toMatchSnapshot()
})
test('text between elements (static)', () => {
const root = transformWithTextOpt(`<div/>hello<div/>`)
expect(root.children.length).toBe(3)
expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
expect(root.children[1]).toMatchObject({
// when mixed with elements, should convert it into a text node call
type: NodeTypes.TEXT_CALL,
codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
TEXT,
`null`,
{
type: NodeTypes.TEXT,
content: `hello`
}
// should have no flag
]
}
}) })
expect(root.children[2].type).toBe(NodeTypes.ELEMENT) expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
@ -70,11 +111,19 @@ describe('compiler: optimize interpolation', () => {
test('consecutive text mixed with elements', () => { test('consecutive text mixed with elements', () => {
const root = transformWithTextOpt( const root = transformWithTextOpt(
`<div/>{{ foo }} bar {{ baz }}<div/>{{ foo }} bar {{ baz }}<div/>` `<div/>{{ foo }} bar {{ baz }}<div/>hello<div/>`
) )
expect(root.children.length).toBe(5) expect(root.children.length).toBe(5)
expect(root.children[0].type).toBe(NodeTypes.ELEMENT) expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
expect(root.children[1]).toMatchObject({ expect(root.children[1]).toMatchObject({
type: NodeTypes.TEXT_CALL,
codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
TEXT,
`null`,
{
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
{ type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, { type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
@ -83,17 +132,26 @@ describe('compiler: optimize interpolation', () => {
` + `, ` + `,
{ type: NodeTypes.INTERPOLATION, content: { content: `baz` } } { type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
] ]
},
genFlagText(PatchFlags.TEXT)
]
}
}) })
expect(root.children[2].type).toBe(NodeTypes.ELEMENT) expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
expect(root.children[3]).toMatchObject({ expect(root.children[3]).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.TEXT_CALL,
children: [ codegenNode: {
{ type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, type: NodeTypes.JS_CALL_EXPRESSION,
` + `, callee: CREATE_VNODE,
{ type: NodeTypes.TEXT, content: ` bar ` }, arguments: [
` + `, TEXT,
{ type: NodeTypes.INTERPOLATION, content: { content: `baz` } } `null`,
{
type: NodeTypes.TEXT,
content: `hello`
}
] ]
}
}) })
expect(root.children[4].type).toBe(NodeTypes.ELEMENT) expect(root.children[4].type).toBe(NodeTypes.ELEMENT)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()

View File

@ -35,6 +35,7 @@ export const enum NodeTypes {
IF, IF,
IF_BRANCH, IF_BRANCH,
FOR, FOR,
TEXT_CALL,
// codegen // codegen
JS_CALL_EXPRESSION, JS_CALL_EXPRESSION,
JS_OBJECT_EXPRESSION, JS_OBJECT_EXPRESSION,
@ -86,6 +87,7 @@ export type TemplateChildNode =
| CommentNode | CommentNode
| IfNode | IfNode
| ForNode | ForNode
| TextCallNode
export interface RootNode extends Node { export interface RootNode extends Node {
type: NodeTypes.ROOT type: NodeTypes.ROOT
@ -227,6 +229,12 @@ export interface ForNode extends Node {
codegenNode: ForCodegenNode codegenNode: ForCodegenNode
} }
export interface TextCallNode extends Node {
type: NodeTypes.TEXT_CALL
content: TextNode | InterpolationNode | CompoundExpressionNode
codegenNode: CallExpression
}
// We also include a number of JavaScript AST nodes for code generation. // We also include a number of JavaScript AST nodes for code generation.
// The AST is an intentionally minimal subset just to meet the exact needs of // The AST is an intentionally minimal subset just to meet the exact needs of
// Vue render function generation. // Vue render function generation.

View File

@ -400,6 +400,9 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
genInterpolation(node, context) genInterpolation(node, context)
break break
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
case NodeTypes.COMPOUND_EXPRESSION: case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context) genCompoundExpression(node, context)
break break

View File

@ -12,7 +12,7 @@ import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind' import { transformBind } from './transforms/vBind'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot' import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { optimizeText } from './transforms/optimizeText' import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce' import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
@ -56,7 +56,7 @@ export function baseCompile(
transformSlotOutlet, transformSlotOutlet,
transformElement, transformElement,
trackSlotScopes, trackSlotScopes,
optimizeText, transformText,
...(options.nodeTransforms || []) // user transforms ...(options.nodeTransforms || []) // user transforms
], ],
directiveTransforms: { directiveTransforms: {

View File

@ -122,6 +122,7 @@ export function isStaticNode(
case NodeTypes.FOR: case NodeTypes.FOR:
return false return false
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL:
return isStaticNode(node.content, resultCache) return isStaticNode(node.content, resultCache)
case NodeTypes.SIMPLE_EXPRESSION: case NodeTypes.SIMPLE_EXPRESSION:
return node.isConstant return node.isConstant

View File

@ -4,8 +4,11 @@ import {
TemplateChildNode, TemplateChildNode,
TextNode, TextNode,
InterpolationNode, InterpolationNode,
CompoundExpressionNode CompoundExpressionNode,
createCallExpression
} from '../ast' } from '../ast'
import { TEXT, CREATE_VNODE } from '../runtimeHelpers'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
const isText = ( const isText = (
node: TemplateChildNode node: TemplateChildNode
@ -14,16 +17,19 @@ const isText = (
// Merge adjacent text nodes and expressions into a single expression // Merge adjacent text nodes and expressions into a single expression
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child. // e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
export const optimizeText: NodeTransform = node => { export const transformText: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ELEMENT) {
// perform the transform on node exit so that all expressions have already // perform the transform on node exit so that all expressions have already
// been processed. // been processed.
return () => { return () => {
const children = node.children const children = node.children
let currentContainer: CompoundExpressionNode | undefined = undefined let currentContainer: CompoundExpressionNode | undefined = undefined
let hasText = false
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (isText(child)) { if (isText(child)) {
hasText = true
for (let j = i + 1; j < children.length; j++) { for (let j = i + 1; j < children.length; j++) {
const next = children[j] const next = children[j]
if (isText(next)) { if (isText(next)) {
@ -45,6 +51,31 @@ export const optimizeText: NodeTransform = node => {
} }
} }
} }
if (hasText && children.length > 1) {
// when an element has mixed text/element children, convert text nodes
// into createVNode(Text) calls.
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) {
const callArgs = [context.helper(TEXT), `null`, child]
if (child.type !== NodeTypes.TEXT) {
callArgs.push(
`${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`
)
}
children[i] = {
type: NodeTypes.TEXT_CALL,
content: child,
loc: child.loc,
codegenNode: createCallExpression(
context.helper(CREATE_VNODE),
callArgs
)
}
}
}
}
} }
} }
} }

View File

@ -293,9 +293,16 @@ export function hasScopeRef(
case NodeTypes.COMPOUND_EXPRESSION: case NodeTypes.COMPOUND_EXPRESSION:
return node.children.some(c => isObject(c) && hasScopeRef(c, ids)) return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL:
return hasScopeRef(node.content, ids) return hasScopeRef(node.content, ids)
case NodeTypes.TEXT:
case NodeTypes.COMMENT:
return false
default: default:
// TextNode or CommentNode if (__DEV__) {
const exhaustiveCheck: never = node
exhaustiveCheck
}
return false return false
} }
} }

View File

@ -488,7 +488,7 @@ export function createRenderer<
} }
return // terminal return // terminal
} }
} else if (!optimized) { } else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff // unoptimized, full diff
patchProps( patchProps(
el, el,