ソースを参照

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.
Evan You 6 年 前
コミット
052febc127

+ 6 - 6
packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap

@@ -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\\"])),

+ 2 - 2
packages/compiler-core/__tests__/transform.spec.ts

@@ -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
         ]
         ]

+ 0 - 68
packages/compiler-core/__tests__/transforms/__snapshots__/optimizeText.spec.ts.snap

@@ -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)
-}"
-`;

+ 84 - 0
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)
+}"
+`;

+ 2 - 2
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 { 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]

+ 86 - 28
packages/compiler-core/__tests__/transforms/optimizeText.spec.ts → packages/compiler-core/__tests__/transforms/transformText.spec.ts

@@ -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,14 +58,52 @@ 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({
-      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(`<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,30 +111,47 @@ 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.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[2].type).toBe(NodeTypes.ELEMENT)
     expect(root.children[3]).toMatchObject({
     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(root.children[4].type).toBe(NodeTypes.ELEMENT)
     expect(generate(root).code).toMatchSnapshot()
     expect(generate(root).code).toMatchSnapshot()

+ 8 - 0
packages/compiler-core/src/ast.ts

@@ -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.

+ 3 - 0
packages/compiler-core/src/codegen.ts

@@ -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

+ 2 - 2
packages/compiler-core/src/index.ts

@@ -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: {

+ 1 - 0
packages/compiler-core/src/transforms/hoistStatic.ts

@@ -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

+ 33 - 2
packages/compiler-core/src/transforms/optimizeText.ts → packages/compiler-core/src/transforms/transformText.ts

@@ -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
+              )
+            }
+          }
+        }
+      }
     }
     }
   }
   }
 }
 }

+ 8 - 1
packages/compiler-core/src/utils.ts

@@ -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
   }
   }
 }
 }

+ 1 - 1
packages/runtime-core/src/createRenderer.ts

@@ -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,