From 052febc12799044a002fcf2ef648376b195f845b Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 21 Oct 2019 15:52:29 -0400 Subject: [PATCH] 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. --- .../__snapshots__/compile.spec.ts.snap | 12 +- .../compiler-core/__tests__/transform.spec.ts | 4 +- .../__snapshots__/optimizeText.spec.ts.snap | 68 ----------- .../__snapshots__/transformText.spec.ts.snap | 84 +++++++++++++ .../transforms/transformElement.spec.ts | 4 +- ...mizeText.spec.ts => transformText.spec.ts} | 114 +++++++++++++----- packages/compiler-core/src/ast.ts | 8 ++ packages/compiler-core/src/codegen.ts | 3 + packages/compiler-core/src/index.ts | 4 +- .../src/transforms/hoistStatic.ts | 1 + .../{optimizeText.ts => transformText.ts} | 35 +++++- packages/compiler-core/src/utils.ts | 9 +- packages/runtime-core/src/createRenderer.ts | 2 +- 13 files changed, 236 insertions(+), 112 deletions(-) delete mode 100644 packages/compiler-core/__tests__/transforms/__snapshots__/optimizeText.spec.ts.snap create mode 100644 packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap rename packages/compiler-core/__tests__/transforms/{optimizeText.spec.ts => transformText.spec.ts} (53%) rename packages/compiler-core/src/transforms/{optimizeText.ts => transformText.ts} (57%) diff --git a/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap index 6414eed9..9374c67c 100644 --- a/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap @@ -5,13 +5,13 @@ exports[`compiler: integration tests function mode 1`] = ` return function render() { 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\\", { id: \\"foo\\", class: bar.baz }, [ - _toString(world.burn()), + _createVNode(_Text, null, _toString(world.burn()), 1 /* TEXT */), (_openBlock(), ok ? _createBlock(\\"div\\", { key: 0 }, \\"yes\\") : _createBlock(_Fragment, { key: 1 }, [\\"no\\"])), @@ -26,7 +26,7 @@ return function render() { `; 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() { const _ctx = this @@ -34,7 +34,7 @@ return function render() { id: \\"foo\\", class: _ctx.bar.baz }, [ - toString(_ctx.world.burn()), + createVNode(Text, null, toString(_ctx.world.burn()), 1 /* TEXT */), (openBlock(), (_ctx.ok) ? createBlock(\\"div\\", { key: 0 }, \\"yes\\") : createBlock(Fragment, { key: 1 }, [\\"no\\"])), @@ -48,7 +48,7 @@ return function render() { `; 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() { const _ctx = this @@ -56,7 +56,7 @@ export default function render() { id: \\"foo\\", class: _ctx.bar.baz }, [ - toString(_ctx.world.burn()), + createVNode(Text, null, toString(_ctx.world.burn()), 1 /* TEXT */), (openBlock(), (_ctx.ok) ? createBlock(\\"div\\", { key: 0 }, \\"yes\\") : createBlock(Fragment, { key: 1 }, [\\"no\\"])), diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index 3119fa01..2190ad11 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -21,7 +21,7 @@ import { transformIf } from '../src/transforms/vIf' import { transformFor } from '../src/transforms/vFor' import { transformElement } from '../src/transforms/transformElement' import { transformSlotOutlet } from '../src/transforms/transformSlotOutlet' -import { optimizeText } from '../src/transforms/optimizeText' +import { transformText } from '../src/transforms/transformText' describe('compiler: transform', () => { test('context state', () => { @@ -243,7 +243,7 @@ describe('compiler: transform', () => { nodeTransforms: [ transformIf, transformFor, - optimizeText, + transformText, transformSlotOutlet, transformElement ] diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/optimizeText.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/optimizeText.spec.ts.snap deleted file mode 100644 index ce575423..00000000 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/optimizeText.spec.ts.snap +++ /dev/null @@ -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) -}" -`; diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap new file mode 100644 index 00000000..9e1f736a --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap @@ -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) +}" +`; diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index 74b2cc87..06ed14e5 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -23,7 +23,7 @@ import { transformOn } from '../../src/transforms/vOn' import { transformBind } from '../../src/transforms/vBind' import { PatchFlags } from '@vue/shared' import { createObjectMatcher, genFlagText } from '../testUtils' -import { optimizeText } from '../../src/transforms/optimizeText' +import { transformText } from '../../src/transforms/transformText' function parseWithElementTransform( template: string, @@ -36,7 +36,7 @@ function parseWithElementTransform( // block as root node const ast = parse(`
${template}
`, options) transform(ast, { - nodeTransforms: [transformElement, optimizeText], + nodeTransforms: [transformElement, transformText], ...options }) const codegenNode = (ast as any).children[0].children[0] diff --git a/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts b/packages/compiler-core/__tests__/transforms/transformText.spec.ts similarity index 53% rename from packages/compiler-core/__tests__/transforms/optimizeText.spec.ts rename to packages/compiler-core/__tests__/transforms/transformText.spec.ts index 3c8c2638..ec43e0dd 100644 --- a/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformText.spec.ts @@ -5,16 +5,19 @@ import { NodeTypes, generate } from '../../src' -import { optimizeText } from '../../src/transforms/optimizeText' +import { transformText } from '../../src/transforms/transformText' import { transformExpression } from '../../src/transforms/transformExpression' 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 = {}) { const ast = parse(template) transform(ast, { nodeTransforms: [ ...(options.prefixIdentifiers ? [transformExpression] : []), - optimizeText, + transformText, transformElement ], ...options @@ -22,7 +25,7 @@ function transformWithTextOpt(template: string, options: CompilerOptions = {}) { return ast } -describe('compiler: optimize interpolation', () => { +describe('compiler: transform text', () => { test('no consecutive text', () => { const root = transformWithTextOpt(`{{ foo }}`) expect(root.children[0]).toMatchObject({ @@ -55,14 +58,52 @@ describe('compiler: optimize interpolation', () => { 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` } } - ] + // 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, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { 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(`
hello
`) + 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(generate(root).code).toMatchSnapshot() @@ -70,30 +111,47 @@ describe('compiler: optimize interpolation', () => { test('consecutive text mixed with elements', () => { const root = transformWithTextOpt( - `
{{ foo }} bar {{ baz }}
{{ foo }} bar {{ baz }}
` + `
{{ foo }} bar {{ baz }}
hello
` ) 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` } } - ] + type: NodeTypes.TEXT_CALL, + codegenNode: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE, + arguments: [ + TEXT, + `null`, + { + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }, + genFlagText(PatchFlags.TEXT) + ] + } }) 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` } } - ] + type: NodeTypes.TEXT_CALL, + codegenNode: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE, + arguments: [ + TEXT, + `null`, + { + type: NodeTypes.TEXT, + content: `hello` + } + ] + } }) expect(root.children[4].type).toBe(NodeTypes.ELEMENT) expect(generate(root).code).toMatchSnapshot() diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 3d0fbd2a..740da9b1 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -35,6 +35,7 @@ export const enum NodeTypes { IF, IF_BRANCH, FOR, + TEXT_CALL, // codegen JS_CALL_EXPRESSION, JS_OBJECT_EXPRESSION, @@ -86,6 +87,7 @@ export type TemplateChildNode = | CommentNode | IfNode | ForNode + | TextCallNode export interface RootNode extends Node { type: NodeTypes.ROOT @@ -227,6 +229,12 @@ export interface ForNode extends Node { 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. // The AST is an intentionally minimal subset just to meet the exact needs of // Vue render function generation. diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 946162af..ba49ad16 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -400,6 +400,9 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) { case NodeTypes.INTERPOLATION: genInterpolation(node, context) break + case NodeTypes.TEXT_CALL: + genNode(node.codegenNode, context) + break case NodeTypes.COMPOUND_EXPRESSION: genCompoundExpression(node, context) break diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 9b3fa799..097f8917 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -12,7 +12,7 @@ import { transformOn } from './transforms/vOn' import { transformBind } from './transforms/vBind' import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot' -import { optimizeText } from './transforms/optimizeText' +import { transformText } from './transforms/transformText' import { transformOnce } from './transforms/vOnce' import { transformModel } from './transforms/vModel' @@ -56,7 +56,7 @@ export function baseCompile( transformSlotOutlet, transformElement, trackSlotScopes, - optimizeText, + transformText, ...(options.nodeTransforms || []) // user transforms ], directiveTransforms: { diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 18646c51..9ef9dda2 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -122,6 +122,7 @@ export function isStaticNode( case NodeTypes.FOR: return false case NodeTypes.INTERPOLATION: + case NodeTypes.TEXT_CALL: return isStaticNode(node.content, resultCache) case NodeTypes.SIMPLE_EXPRESSION: return node.isConstant diff --git a/packages/compiler-core/src/transforms/optimizeText.ts b/packages/compiler-core/src/transforms/transformText.ts similarity index 57% rename from packages/compiler-core/src/transforms/optimizeText.ts rename to packages/compiler-core/src/transforms/transformText.ts index 93bd15b7..1251a30d 100644 --- a/packages/compiler-core/src/transforms/optimizeText.ts +++ b/packages/compiler-core/src/transforms/transformText.ts @@ -4,8 +4,11 @@ import { TemplateChildNode, TextNode, InterpolationNode, - CompoundExpressionNode + CompoundExpressionNode, + createCallExpression } from '../ast' +import { TEXT, CREATE_VNODE } from '../runtimeHelpers' +import { PatchFlags, PatchFlagNames } from '@vue/shared' const isText = ( node: TemplateChildNode @@ -14,16 +17,19 @@ const isText = ( // Merge adjacent text nodes and expressions into a single expression // e.g.
abc {{ d }} {{ e }}
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) { // perform the transform on node exit so that all expressions have already // been processed. return () => { const children = node.children let currentContainer: CompoundExpressionNode | undefined = undefined + let hasText = false + for (let i = 0; i < children.length; i++) { const child = children[i] if (isText(child)) { + hasText = true for (let j = i + 1; j < children.length; j++) { const next = children[j] 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 + ) + } + } + } + } } } } diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index aed08458..2017d691 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -293,9 +293,16 @@ export function hasScopeRef( case NodeTypes.COMPOUND_EXPRESSION: return node.children.some(c => isObject(c) && hasScopeRef(c, ids)) case NodeTypes.INTERPOLATION: + case NodeTypes.TEXT_CALL: return hasScopeRef(node.content, ids) + case NodeTypes.TEXT: + case NodeTypes.COMMENT: + return false default: - // TextNode or CommentNode + if (__DEV__) { + const exhaustiveCheck: never = node + exhaustiveCheck + } return false } } diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index f2bc0715..572c7ddc 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -488,7 +488,7 @@ export function createRenderer< } return // terminal } - } else if (!optimized) { + } else if (!optimized && dynamicChildren == null) { // unoptimized, full diff patchProps( el,