]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: append multiple node
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Sun, 26 Nov 2023 21:16:21 +0000 (05:16 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Sun, 26 Nov 2023 21:16:21 +0000 (05:16 +0800)
packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/ir.ts
packages/compiler-vapor/src/transform.ts
packages/runtime-vapor/src/render.ts
playground/src/dynamic-all.vue [moved from playground/src/all-dynamic.vue with 100% similarity]
playground/src/dynamic-mixed-mid.vue [new file with mode: 0644]
playground/src/dynamic-mixed.vue [moved from playground/src/dynamic.vue with 100% similarity]

index 4fa319bd189f7a0faf2bfd825dfb4a9043479ef7..020a7f31e86c60d69af837c996be599cf0d0d240 100644 (file)
@@ -8,16 +8,16 @@ export function render() {
   const n0 = t0();
   const {
     0: [
-      n1,
+      n3,
       {
-        1: [n3],
+        1: [n2],
       },
     ],
   } = children(n0);
-  const n2 = createTextNode(count.value);
-  insert(n2, n1, n3);
+  const n1 = createTextNode(count.value);
+  insert(n1, n3, n2);
   watchEffect(() => {
-    setText(n2, undefined, count.value);
+    setText(n1, undefined, count.value);
   });
   return n0;
 }
@@ -110,22 +110,22 @@ export function render() {
 `;
 
 exports[`comile > directives > v-once > basic 1`] = `
-"import { template, children, createTextNode, insert, setText, setAttr } from 'vue/vapor';
+"import { template, children, createTextNode, setText, setAttr, insert } from 'vue/vapor';
 const t0 = template('<div> <span></span></div>');
 export function render() {
   const n0 = t0();
   const {
     0: [
-      n1,
+      n3,
       {
-        1: [n3],
+        2: [n2],
       },
     ],
   } = children(n0);
-  const n2 = createTextNode(msg.value);
-  insert(n2, n1, 0 /* InsertPosition.FIRST */);
-  setText(n2, undefined, msg.value);
-  setAttr(n3, 'class', undefined, clz.value);
+  const n1 = createTextNode(msg.value);
+  setText(n1, undefined, msg.value);
+  setAttr(n2, 'class', undefined, clz.value);
+  insert(n1, n3, 0 /* InsertPosition.FIRST */);
   return n0;
 }
 "
@@ -167,14 +167,13 @@ export function render() {
 
 exports[`comile > dynamic root 1`] = `
 "import { watchEffect } from 'vue';
-import { fragment, createTextNode, insert, setText } from 'vue/vapor';
+import { fragment, createTextNode, append, setText } from 'vue/vapor';
 export function render() {
   const t0 = fragment();
   const n0 = t0();
   const n1 = createTextNode(1);
-  insert(n1, n0, 0 /* InsertPosition.FIRST */);
   const n2 = createTextNode(2);
-  insert(n2, n0);
+  append(n0, n1, n2);
   watchEffect(() => {
     setText(n1, undefined, 1);
   });
@@ -203,8 +202,8 @@ const t0 = template('2');
 export function render() {
   const n0 = t0();
   const n1 = createTextNode(1);
-  insert(n1, n0, 0 /* InsertPosition.FIRST */);
   const n2 = createTextNode(3);
+  insert(n1, n0, 0 /* InsertPosition.FIRST */);
   insert(n2, n0);
   watchEffect(() => {
     setText(n1, undefined, 1);
index 0c20d725a39bc3c69050dd28f4dabcb01ff63b06..323534710bb99c8b3b8a5a4ccfb97fd3fd1f5fa7 100644 (file)
@@ -20,19 +20,19 @@ const increment = () => count.value++
 
 return (() => {
 const n0 = t0()
-const { 1: [n1], 2: [n3], 3: [n5], 4: [n6], 6: [n7],} = children(n0)
-const n2 = createTextNode(count.value)
-insert(n2, n1)
-const n4 = createTextNode(double.value)
-insert(n4, n3)
-const n8 = createTextNode(count.value)
-insert(n8, n7)
-setText(n8, undefined, count.value)
+const { 1: [n2], 2: [n4], 3: [n5], 4: [n6], 6: [n8],} = children(n0)
+const n1 = createTextNode(count.value)
+insert(n1, n2)
+const n3 = createTextNode(double.value)
+insert(n3, n4)
+const n7 = createTextNode(count.value)
+setText(n7, undefined, count.value)
+insert(n7, n8)
 watchEffect(() => {
-setText(n2, undefined, count.value)
+setText(n1, undefined, count.value)
 })
 watchEffect(() => {
-setText(n4, undefined, double.value)
+setText(n3, undefined, double.value)
 })
 watchEffect(() => {
 on(n5, \\"click\\", increment)
index f1d20ab0b37e538205c52b604515cccce0fb6537..d101fcf696fe35db1ae59da56ce44e82fe0344ed 100644 (file)
@@ -33,11 +33,11 @@ export function generate(
   })
 
   {
-    code += `const n${ir.children.id} = t0()\n`
-    if (Object.keys(ir.children.children).length) {
-      code += `const {${genChildren(ir.children.children)}} = children(n${
-        ir.children.id
-      })\n`
+    code += `const n${ir.dynamic.id} = t0()\n`
+
+    const children = genChildren(ir.dynamic.children)
+    if (children) {
+      code += `const ${children} = children(n${ir.dynamic.id})\n`
       vaporHelpers.add('children')
     }
 
@@ -56,7 +56,7 @@ export function generate(
     }
     // TODO multiple-template
     // TODO return statement in IR
-    code += `return n${ir.children.id}\n`
+    code += `return n${ir.dynamic.id}\n`
   }
 
   if (vaporHelpers.size)
@@ -79,59 +79,65 @@ export function generate(
     preamble,
   }
 
-  function genOperation(operation: OperationNode) {
+  function genOperation(oper: OperationNode) {
     let code = ''
 
     // TODO: cache old value
-    switch (operation.type) {
+    switch (oper.type) {
       case IRNodeTypes.SET_PROP: {
-        code = `setAttr(n${operation.element}, ${JSON.stringify(
-          operation.name,
-        )}, undefined, ${operation.value})\n`
+        code = `setAttr(n${oper.element}, ${JSON.stringify(
+          oper.name,
+        )}, undefined, ${oper.value})\n`
         vaporHelpers.add('setAttr')
         break
       }
 
       case IRNodeTypes.SET_TEXT: {
-        code = `setText(n${operation.element}, undefined, ${operation.value})\n`
+        code = `setText(n${oper.element}, undefined, ${oper.value})\n`
         vaporHelpers.add('setText')
         break
       }
 
       case IRNodeTypes.SET_EVENT: {
-        code = `on(n${operation.element}, ${JSON.stringify(operation.name)}, ${
-          operation.value
+        code = `on(n${oper.element}, ${JSON.stringify(oper.name)}, ${
+          oper.value
         })\n`
         vaporHelpers.add('on')
         break
       }
 
       case IRNodeTypes.SET_HTML: {
-        code = `setHtml(n${operation.element}, undefined, ${operation.value})\n`
+        code = `setHtml(n${oper.element}, undefined, ${oper.value})\n`
         vaporHelpers.add('setHtml')
         break
       }
 
       case IRNodeTypes.CREATE_TEXT_NODE: {
-        code = `const n${operation.id} = createTextNode(${operation.value})\n`
+        code = `const n${oper.id} = createTextNode(${oper.value})\n`
         vaporHelpers.add('createTextNode')
         break
       }
 
       case IRNodeTypes.INSERT_NODE: {
         let anchor = ''
-        if (typeof operation.anchor === 'number') {
-          anchor = `, n${operation.anchor}`
-        } else if (operation.anchor === 'first') {
+        if (typeof oper.anchor === 'number') {
+          anchor = `, n${oper.anchor}`
+        } else if (oper.anchor === 'first') {
           anchor = `, 0 /* InsertPosition.FIRST */`
         }
-        code = `insert(n${operation.element}, n${operation.parent}${anchor})\n`
+        code = `insert(n${oper.element}, n${oper.parent}${anchor})\n`
         vaporHelpers.add('insert')
         break
       }
-
+      case IRNodeTypes.APPEND_NODE: {
+        code = `append(n${oper.parent}, ${oper.elements
+          .map((el) => `n${el}`)
+          .join(', ')})\n`
+        vaporHelpers.add('append')
+        break
+      }
       default:
-        checkNever(operation)
+        checkNever(oper)
     }
 
     return code
@@ -139,16 +145,26 @@ export function generate(
 }
 
 function genChildren(children: DynamicChildren) {
-  let str = ''
+  let code = ''
+  // TODO
+  let offset = 0
+
   for (const [index, child] of Object.entries(children)) {
-    str += ` ${index}: [`
-    if (child.store) {
-      str += `n${child.id}`
-    }
-    if (Object.keys(child.children).length) {
-      str += `, {${genChildren(child.children)}}`
-    }
-    str += '],'
+    const childrenLength = Object.keys(child.children).length
+    if (child.ghost && child.placeholder === null && childrenLength === 0)
+      continue
+
+    code += ` ${Number(index) + offset}: [`
+
+    const id = child.ghost ? child.placeholder : child.id
+    if (id !== null) code += `n${id}`
+
+    const childrenString = childrenLength && genChildren(child.children)
+    if (childrenString) code += `, ${childrenString}`
+
+    code += '],'
   }
-  return str
+
+  if (!code) return ''
+  return `{${code}}`
 }
index d69e522b425e96baa91c395188f279ccfb44c60f..bd4909fe588e1f1213138cb414b43663ccdd46cc 100644 (file)
@@ -11,6 +11,7 @@ export const enum IRNodeTypes {
   SET_HTML,
 
   INSERT_NODE,
+  APPEND_NODE,
   CREATE_TEXT_NODE,
 }
 
@@ -22,7 +23,7 @@ export interface IRNode {
 export interface RootIRNode extends IRNode {
   type: IRNodeTypes.ROOT
   template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
-  children: DynamicChild
+  dynamic: DynamicInfo
   // TODO multi-expression effect
   effect: Record<string /* expr */, OperationNode[]>
   operation: OperationNode[]
@@ -71,11 +72,18 @@ export interface CreateTextNodeIRNode extends IRNode {
   value: string
 }
 
+export type InsertAnchor = number | 'first' | 'last'
 export interface InsertNodeIRNode extends IRNode {
   type: IRNodeTypes.INSERT_NODE
   element: number
   parent: number
-  anchor: number | 'first' | 'last'
+  anchor: InsertAnchor
+}
+
+export interface AppendNodeIRNode extends IRNode {
+  type: IRNodeTypes.APPEND_NODE
+  elements: number[]
+  parent: number
 }
 
 export type OperationNode =
@@ -85,11 +93,14 @@ export type OperationNode =
   | SetHtmlIRNode
   | CreateTextNodeIRNode
   | InsertNodeIRNode
+  | AppendNodeIRNode
 
-export interface DynamicChild {
+export interface DynamicInfo {
   id: number | null
-  store: boolean
+  referenced: boolean
+  /** created by DOM API */
   ghost: boolean
+  placeholder: number | null
   children: DynamicChildren
 }
-export type DynamicChildren = Record<number, DynamicChild>
+export type DynamicChildren = Record<number, DynamicInfo>
index 00e9de330b81d4729d75dca9d9151d2808932bf2..a5dee2a356034a59b7cd50f8f9ce73f26ef89fed 100644 (file)
@@ -11,10 +11,11 @@ import type {
   ExpressionNode,
 } from '@vue/compiler-dom'
 import {
-  type DynamicChildren,
   type OperationNode,
   type RootIRNode,
   IRNodeTypes,
+  DynamicInfo,
+  InsertAnchor,
 } from './ir'
 import { isVoidTag } from '@vue/shared'
 
@@ -24,14 +25,13 @@ export interface TransformContext<T extends Node = Node> {
   root: TransformContext<RootNode>
   index: number
   options: TransformOptions
+
   template: string
-  children: DynamicChildren
-  store: boolean
-  ghost: boolean
+  dynamic: DynamicInfo
+
   once: boolean
-  id: number | null
 
-  getId(): number
+  reference(): number
   incraseId(): number
   registerTemplate(): number
   registerEffect(expr: string, operation: OperationNode): void
@@ -53,18 +53,19 @@ function createRootContext(
     index: 0,
     root: undefined as any, // set later
     options,
-    children: {},
-    store: false,
-    ghost: false,
+    dynamic: ir.dynamic,
     once: false,
 
-    id: null,
     incraseId: () => globalId++,
-    getId() {
-      if (this.id !== null) return this.id
-      return (this.id = this.incraseId())
+    reference() {
+      if (this.dynamic.id !== null) return this.dynamic.id
+      this.dynamic.referenced = true
+      return (this.dynamic.id = this.incraseId())
     },
     registerEffect(expr, operation) {
+      if (this.once) {
+        return this.registerOpration(operation)
+      }
       if (!effect[expr]) effect[expr] = []
       effect[expr].push(operation)
     },
@@ -97,6 +98,7 @@ function createRootContext(
     },
   }
   ctx.root = ctx
+  ctx.reference()
   return ctx
 }
 
@@ -105,27 +107,19 @@ function createContext<T extends TemplateChildNode>(
   parent: TransformContext,
   index: number,
 ): TransformContext<T> {
-  const children = {}
-
   const ctx: TransformContext<T> = {
     ...parent,
-    id: null,
     node,
     parent,
     index,
-    get template() {
-      return parent.template
-    },
-    set template(t) {
-      parent.template = t
-    },
-    children,
-    store: false,
-    registerEffect(expr, operation) {
-      if (ctx.once) {
-        return ctx.registerOpration(operation)
-      }
-      return parent.registerEffect(expr, operation)
+
+    template: '',
+    dynamic: {
+      id: null,
+      referenced: false,
+      ghost: false,
+      placeholder: null,
+      children: {},
     },
   }
   return ctx
@@ -140,23 +134,22 @@ export function transform(
     type: IRNodeTypes.ROOT,
     loc: root.loc,
     template: [],
-    children: {} as any,
+    dynamic: {
+      id: null,
+      referenced: true,
+      ghost: true,
+      placeholder: null,
+      children: {},
+    },
     effect: Object.create(null),
     operation: [],
     helpers: new Set([]),
     vaporHelpers: new Set([]),
   }
   const ctx = createRootContext(ir, root, options)
-  const rootId = ctx.getId()
 
   // TODO: transform presets, see packages/compiler-core/src/transforms
   transformChildren(ctx, true)
-  ir.children = {
-    id: rootId,
-    store: true,
-    ghost: false,
-    children: ctx.children,
-  }
   if (ir.template.length === 0) {
     ir.template.push({
       type: IRNodeTypes.FRAGMENT_FACTORY,
@@ -174,15 +167,57 @@ function transformChildren(
   const {
     node: { children },
   } = ctx
-  let index = 0
+  const childrenTemplate: string[] = []
   children.forEach((child, i) => walkNode(child, i))
 
+  const dynamicChildren = Object.values(ctx.dynamic.children)
+  const dynamicCount = dynamicChildren.reduce(
+    (prev, child) => prev + (child.ghost ? 1 : 0),
+    0,
+  )
+  if (dynamicCount === children.length) {
+    // all dynamic node
+    ctx.registerOpration({
+      type: IRNodeTypes.APPEND_NODE,
+      loc: ctx.node.loc,
+      elements: dynamicChildren.map((child) => child.id!),
+      parent: ctx.reference(),
+    })
+  } else if (dynamicCount > 0 && dynamicCount < children.length) {
+    // mixed
+    for (const [indexString, child] of Object.entries(ctx.dynamic.children)) {
+      if (!child.ghost) continue
+
+      const index = Number(indexString)
+      let anchor: InsertAnchor
+      if (index === 0) {
+        anchor = 'first'
+      } else if (index === children.length - 1) {
+        anchor = 'last'
+      } else {
+        childrenTemplate[index] = `<!>`
+        anchor = child.placeholder = ctx.incraseId()
+      }
+
+      ctx.registerOpration({
+        type: IRNodeTypes.INSERT_NODE,
+        loc: ctx.node.loc,
+        element: child.id!,
+        parent: ctx.reference(),
+        anchor,
+      })
+    }
+  }
+
+  ctx.template += childrenTemplate.join('')
+
+  // finalize template
   if (root) ctx.registerTemplate()
 
-  function walkNode(node: TemplateChildNode, i: number) {
+  function walkNode(node: TemplateChildNode, index: number) {
     const child = createContext(node, ctx, index)
-    const isFirst = i === 0
-    const isLast = i === children.length - 1
+    const isFirst = index === 0
+    const isLast = index === children.length - 1
 
     switch (node.type) {
       case 1 satisfies NodeTypes.ELEMENT: {
@@ -190,11 +225,11 @@ function transformChildren(
         break
       }
       case 2 satisfies NodeTypes.TEXT: {
-        ctx.template += node.content
+        child.template += node.content
         break
       }
       case 3 satisfies NodeTypes.COMMENT: {
-        ctx.template += `<!--${node.content}-->`
+        child.template += `<!--${node.content}-->`
         break
       }
       case 5 satisfies NodeTypes.INTERPOLATION: {
@@ -214,19 +249,20 @@ function transformChildren(
         // IfNode
         // IfBranchNode
         // ForNode
-        ctx.template += `[type: ${node.type}]`
+        child.template += `[type: ${node.type}]`
       }
     }
 
-    if (Object.keys(child.children).length > 0 || child.store)
-      ctx.children[index] = {
-        id: child.store ? child.getId() : null,
-        store: child.store,
-        children: child.children,
-        ghost: child.ghost,
-      }
+    childrenTemplate.push(child.template)
 
-    if (!child.ghost) index++
+    if (
+      child.dynamic.ghost ||
+      child.dynamic.referenced ||
+      child.dynamic.placeholder ||
+      Object.keys(child.dynamic.children).length
+    ) {
+      ctx.dynamic.children[index] = child.dynamic
+    }
   }
 }
 
@@ -254,62 +290,38 @@ function transformInterpolation(
 ) {
   const { node } = ctx
 
-  if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) {
-    const expr = processExpression(ctx, node.content)!
-
-    const parent = ctx.parent!
-    const parentId = parent.getId()
-    parent.store = true
-
-    if (isFirst && isLast) {
-      ctx.registerEffect(expr, {
-        type: IRNodeTypes.SET_TEXT,
-        loc: node.loc,
-        element: parentId,
-        value: expr,
-      })
-    } else {
-      let id: number
-      let anchor: number | 'first' | 'last'
-
-      if (!isFirst && !isLast) {
-        id = ctx.incraseId()
-        anchor = ctx.getId()
-        ctx.template += '<!>'
-        ctx.store = true
-      } else {
-        id = ctx.getId()
-        ctx.ghost = true
-        anchor = isFirst ? 'first' : 'last'
-      }
-
-      ctx.registerOpration(
-        {
-          type: IRNodeTypes.CREATE_TEXT_NODE,
-          loc: node.loc,
-          id,
-          value: expr,
-        },
-        {
-          type: IRNodeTypes.INSERT_NODE,
-          loc: node.loc,
-          element: id,
-          parent: parentId,
-          anchor,
-        },
-      )
-
-      ctx.registerEffect(expr, {
-        type: IRNodeTypes.SET_TEXT,
-        loc: node.loc,
-        element: id,
-        value: expr,
-      })
-    }
+  if (node.content.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
+    // TODO: CompoundExpressionNode: {{ count + 1 }}
     return
   }
 
-  // TODO: CompoundExpressionNode: {{ count + 1 }}
+  const expr = processExpression(ctx, node.content)!
+
+  if (isFirst && isLast) {
+    const parent = ctx.parent!
+    const parentId = parent.reference()
+    ctx.registerEffect(expr, {
+      type: IRNodeTypes.SET_TEXT,
+      loc: node.loc,
+      element: parentId,
+      value: expr,
+    })
+  } else {
+    const id = ctx.reference()
+    ctx.dynamic.ghost = true
+    ctx.registerOpration({
+      type: IRNodeTypes.CREATE_TEXT_NODE,
+      loc: node.loc,
+      id,
+      value: expr,
+    })
+    ctx.registerEffect(expr, {
+      type: IRNodeTypes.SET_TEXT,
+      loc: node.loc,
+      element: id,
+      value: expr,
+    })
+  }
 }
 
 function transformProp(
@@ -327,7 +339,6 @@ function transformProp(
     return
   }
 
-  ctx.store = true
   const expr = processExpression(ctx, node.exp)
   switch (name) {
     case 'bind': {
@@ -348,7 +359,7 @@ function transformProp(
       ctx.registerEffect(expr, {
         type: IRNodeTypes.SET_PROP,
         loc: node.loc,
-        element: ctx.getId(),
+        element: ctx.reference(),
         name: node.arg.content,
         value: expr,
       })
@@ -372,7 +383,7 @@ function transformProp(
       ctx.registerEffect(expr, {
         type: IRNodeTypes.SET_EVENT,
         loc: node.loc,
-        element: ctx.getId(),
+        element: ctx.reference(),
         name: node.arg.content,
         value: expr,
       })
@@ -383,7 +394,7 @@ function transformProp(
       ctx.registerEffect(value, {
         type: IRNodeTypes.SET_HTML,
         loc: node.loc,
-        element: ctx.getId(),
+        element: ctx.reference(),
         value,
       })
       break
@@ -393,7 +404,7 @@ function transformProp(
       ctx.registerEffect(value, {
         type: IRNodeTypes.SET_TEXT,
         loc: node.loc,
-        element: ctx.getId(),
+        element: ctx.reference(),
         value,
       })
       break
index 43ce1ad932404734738766942ef3f5f39b713bf4..9132c202212303115081f26c78bc7c84120dac9d 100644 (file)
@@ -57,6 +57,10 @@ export function insert(
   // }
 }
 
+export function append(parent: ParentNode, ...nodes: (Node | string)[]) {
+  parent.append(...nodes)
+}
+
 export function remove(block: Block, parent: ParentNode) {
   if (block instanceof Node) {
     parent.removeChild(block)
@@ -124,6 +128,6 @@ export function children(n: ChildNode): Children {
   return { ...Array.from(n.childNodes).map(n => [n, children(n)]) }
 }
 
-export function createTextNode(data: string): Text {
-  return document.createTextNode(data)
+export function createTextNode(val: unknown): Text {
+  return document.createTextNode(toDisplayString(val))
 }
diff --git a/playground/src/dynamic-mixed-mid.vue b/playground/src/dynamic-mixed-mid.vue
new file mode 100644 (file)
index 0000000..6376819
--- /dev/null
@@ -0,0 +1,4 @@
+<template>
+  1{{ 2 }}{{ 3 }}4
+  <div>div</div>
+</template>