codegen 生成联合 3 种类型

在本小节中,我们将会实现 codegen 生成联合 3 种类型的 code

<div>hi,{{message}}</div>
// 生成为
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString } = Vue
export function render(_ctx, _cache) { return _createElementVNode(
'div', null, 'hi,' + _toDisplayString(_ctx.message)) }

1. 测试样例

test('union 3 type', () => {
  const template = '<div>hi,{{message}}</div>'
  const ast = baseParse(template)
  transform(ast)
  const code = codegen(ast)
  expect(code).toMatchSnapshot()
})

2. 实现

此时我们直接生成快照,发现是有问题的,这是因为我们在 genElement 的时候没有考虑到 children

function genElement(node, context) {
  const { push, helper } = context
  const { tag } = node
  push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}'`)
  // 加入对 children 的处理
  const { children } = node
  if (children.length) {
    push(', null, ')
    for (let i = 0; i < children.length; i++) {
      genNode(children[i], context)
    }
  }
  push(')')
}

此时来看看我们的快照:

const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString } = Vue
export function render(_ctx, _cache) { return _createElementVNode('div', null, 'hi,'_toDisplayString(_ctx.message)) }

此时我们发现一个问题,那就是没有加号。所以我们可以再创建一个类型:compound 复合类型:

  • 如果 text 、interpolation 相邻在一起,那么相邻在一起的就是 compound 类型

2.1 复合类型处理

首先,加入一种类型:

export const enum NodeType {
  INTERPOLATION,
  SIMPLE_EXPRESSION,
  ELEMENT,
  TEXT,
  ROOT,
  // 加入复合类型
  COMPOUND_EXPRESSION,
}

此时我们需要去增加一个 transformText

import { NodeType } from '../ast'

export function transformText(node) {
  const { children } = node
  if (children.length) {
    let currentContainer
    for (let i = 0; i < children.length; i++) {
      const child = children[i]
      if (isText(child)) {
        for (let j = i + 1; j < children.length; j++) {
          const next = children[j]
          if (isText(next)) {
            // 相邻的是 text 或者 interpolation,那么就变成联合类型
            if (!currentContainer) {
              currentContainer = children[i] = {
                type: NodeType.COMPOUND_EXPRESSION,
                children: [child],
              }
            }
            // 在每个相邻的下一个之前加上一个 +
            currentContainer.children.push(' + ')
            currentContainer.children.push(next)
            // 遇到就删除
            children.splice(j, 1)
            // 修正索引,因为我们下一个循环就又 + 1 了。此时索引就不对了
            j -= 1
          } else {
            // 如果下一个不是 text 的了,那么就重置,并跳出循环
            currentContainer = undefined
            break
          }
        }
      }
    }
  }
}

function isText(node) {
  return node.type === NodeType.TEXT || node.type === NodeType.INTERPOLATION
}

然后在测试中加入这个 plugin

test('union 3 type', () => {
    const template = '<div>hi,{{message}}</div>'
    const ast = baseParse(template)
    transform(ast, {
      // 加入 transformText plugin
      nodeTransforms: [transformElement, transformExpression, transformText],
    })
    const code = codegen(ast)
    expect(code).toMatchSnapshot()
  })

然后我们就可以在 codegen 阶段加入对 COMPOUD 类型的处理

function genNode(node, context) {
  switch (node.type) {
    case NodeType.TEXT:
      genText(node, context)
      break
    case NodeType.INTERPOLATION:
      genInterpolation(node, context)
      break
    case NodeType.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeType.ELEMENT:
      genElement(node, context)
      break
    // 加入对 compound 类型的处理
    case NodeType.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context)
  }
}

function genCompoundExpression(node, context) {
  const { children } = node
  const { push } = context
  // 对 children 进行遍历
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    // 如果是 string,也就是我们手动添加的 +
    if (isString(child)) {
      // 直接 push
      push(child)
    } else {
      // 否则还是走 genNode
      genNode(child, context)
    }
  }
}

此时我们就可以生成了。

2.2 优化 genElement

此时我们回过头来看看 genElement 我们发现是有问题的:

function genElement(node, context) {
  // 我们这里写死了 props 给 null,以及直接用 tag
  const { push, helper } = context
  const { tag } = node
  push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}'`)
  const { children } = node
  if (children.length) {
    push(', null, ')
    for (let i = 0; i < children.length; i++) {
      genNode(children[i], context)
    }
  }
  push(')')
}

我们需要一个兼容层来处理 props 和 tag。在哪里处理呢?就是在 transformElement 的地方进行处理

export function transformElement(node, context) {
  if (node.type === NodeType.ELEMENT) {
    context.helper(CREATE_ELEMENT_VNODE)
    // 中间处理层,处理 props 和 tag
    const vnodeTag = node.tag
    const vnodeProps = node.props

    const { children } = node
    let vnodeChildren = children

    const vnodeElement = {
      type: NodeType.ELEMENT,
      tag: vnodeTag,
      props: vnodeProps,
      children: vnodeChildren,
    }

    node.codegenNode = vnodeElement
  }
}
function createRootCodegen(root) {
  const child = root.children[0]
  // 在这里进行判断,如果说 children[0] 的类型是 ELEMENT,那么直接修改为 child.codegenNode
  if (child.type === NodeType.ELEMENT) {
    root.codegenNode = child.codegenNode
  } else {
    root.codegenNode = root.children[0]
  }
}

然后就可以修改 genElement

function genElement(node, context) {
  const { push, helper } = context
  const { tag, props } = node
  push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}'`)
  const { children } = node
  if (children.length) {
    // 这里的 props 就可以是活的了
    push(`, ${props}, `)
    for (let i = 0; i < children.length; i++) {
      genNode(children[i], context)
    }
  }
  push(')')
}

2.3 优化插件执行顺序

现在我们又发现了一个问题,虽然我们仍然引入了 transformExpression 插件,但是我们的 interpolation 还是没有 _ctx。这是因为我们修改了结构,此时第二层结构还存在一个 COMPOUND_EXPRESSION。那么我们就需要让 transformExpression 最先执行,他执行完毕后,再去执行其他的插件。也就是优化插件的执行顺序。

我们设计是这样的:

  • 如果一个插件执行返回的是一个函数,那么表示该插件会是一个退出执行函数
  • 在最后,会执行所有的退出函数
function traverseNode(node, context) {
  const { nodeTransforms } = context
  const exitFns: any[] = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const transform = nodeTransforms[i]
    const exitFn = transform(node, context)
    // 收集退出函数
    if (exitFn) exitFns.push(exitFn)
  }
  switch (node.type) {
    case NodeType.INTERPOLATION:
      context.helper(TO_DISPLAY_STRING)
      break
    case NodeType.ROOT:
    case NodeType.ELEMENT:
      traverseChildren(node, context)
      break
    default:
      break
  }
  let i = exitFns.length
  // 执行所有的退出函数
  while (i--) {
    exitFns[i]()
  }
}

2.4 优化空值

function genElement(node, context) {
  const { push, helper } = context
  const { tag, props } = node
  push(`${helper(CREATE_ELEMENT_VNODE)}(`)
  const { children } = node
  // 在这里批量处理 tag,props 和 children,优化空值情况
  genNodeList(genNullable([tag, props, children]), context)
  push(')')
}

function genNodeList(nodes, context) {
  const { push } = context
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (isString(node)) {
      push(node)
    } else if (isArray(node)) {
      for (let j = 0; j < node.length; j++) {
        const n = node[j]
        genNode(n, context)
      }
    } else {
      genNode(node, context)
    }
    if (i < nodes.length - 1) {
      push(', ')
    }
  }
}

function genNullable(args) {
  return args.map(arg => arg || 'null')
}

3. 重构

3.1 抽离 vnode

export function transformElement(node, context) {
  return () => {
    if (node.type === NodeType.ELEMENT) {
      context.helper(CREATE_ELEMENT_VNODE)
      const vnodeTag = `'${node.tag}'`
      const vnodeProps = node.props

      const { children } = node
      let vnodeChildren = children
			// 这里可以抽离
      const vnodeElement = {
        type: NodeType.ELEMENT,
        tag: vnodeTag,
        props: vnodeProps,
        children: vnodeChildren,
      }

      node.codegenNode = vnodeElement
    }
  }
}
export function transformElement(node, context) {
  if (node.type === NodeType.ELEMENT) {
    return () => {
      // 中间处理层,处理 props 和 tag
      const vnodeTag = `'${node.tag}'`
      const vnodeProps = node.props

      const { children } = node
      const vnodeChildren = children
			// 抽离函数
      node.codegenNode = createVNodeCall(
        context,
        vnodeTag,
        vnodeProps,
        vnodeChildren
      )
    }
  }
}

// ast.ts

export function createVNodeCall(context, tag, props, children) {
  context.helper(CREATE_ELEMENT_VNODE)
  return {
    type: NodeType.ELEMENT,
    tag,
    props,
    children,
  }
}

3.2 抽离 isText

还可以将 transformText 中的 isText 抽离到 utils.ts 中。