transform 模块

在本小节中,我们将通过一个小例子,来看看 transform 模块的作用

1. 测试样例

test('should change text content', () => {
  const ast = baseParse('<div>hi</div>')
  transform(ast)
  expect(ast.children[0].children[0].content).toEqual('hi mini-vue')
})

我们这个测试样例要保证的就是,如果说 nodeType 是 text 的时候,修改其 content 的值。

2. 实现

实现起来还是非常简单的,我们只需要遍历整个树就可以了,这里我们使用递归来遍历。

export function transform(root) {
  traverseNode(root)
}

function traverseNode(node) {
  // 在这里就可以对 node 进行操作
  const children = node.children
  if (children) {
    for (let i = 0; i < children.length; i++) {
      traverseNode(children[i])
    }
  }
}

但是我们发现,其实将 content 的值追加 mini-vue 这类的需求,其实是非常特定的场景下的,但是我们设计程序是肯定要通用性最佳的,不可能要将特定的处理写在程序中。

所以我们就可以换一种思路,通过外部提供处理程序,内部再调用外部传入的处理程序。我们称之为插件(plugin)。

// 改写测试
test('should change text content', () => {
  const ast = baseParse('<div>hi</div>')
  // 外部提供处理
   const transformText = node => {
     if (node.type === NodeType.TEXT) {
       node.content += ' mini-vue'
     }
   }
  // 通过 options 传入内部,内部再调用
  transform(ast, {
    nodeTransforms: [transformText],
  })
  expect(ast.children[0].children[0].content).toEqual('hi mini-vue')
})
export function transform(root, options) {
  // 首先我们创建一个 transform 的上下文
  const context = createTransformContext(root, options)
  // 然后将这个上下文传入 traverseNode 中
  traverseNode(root, context)
}

function createTransformContext(root, options) {
  return {
    root,
    nodeTransforms: options.nodeTransforms || {},
  }
}

function traverseNode(node, context) {
  // 在这里对每个 node 通过 transforms 进行依次处理
  const { nodeTransforms } = context
  for (let i = 0; i < nodeTransforms.length; i++) {
    const transform = nodeTransforms[i]
    transform(node)
  }
  const children = node.children
  if (children) {
    for (let i = 0; i < children.length; i++) {
      traverseNode(children[i], context)
    }
  }
}

这样,我们就可以通过外部传入的处理程序来对于内部的 node 进行处理了。

3. 重构

我们可以将 traverseNode 中的部分代码抽离出去

function traverseNode(node, context) {
  const { nodeTransforms } = context
  for (let i = 0; i < nodeTransforms.length; i++) {
    const transform = nodeTransforms[i]
    transform(node)
  }
  // 将递归的部分抽离出去
  traverseChildren(node, context)
}

function traverseChildren(node, context) {
  const children = node.children
  if (children) {
    for (let i = 0; i < children.length; i++) {
      traverseNode(children[i], context)
    }
  }
}

4. 为下个阶段 codegen 铺路

我们在看整个 compiler 模块的流程时,我们发现 transform 下个阶段就是 codegen,生成最终的 js string。那么目前我们在 codegen 模块可以直接对整个 ast 树进行做处理吗?肯定是不行的, 假设我们后期想要修改 ast 的结构,那么肯定还要修改 codegen 的代码。这是不合理的。

export function transform(root, options = {}) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  createRootCodegen(root)
}

function createRootCodegen(root) {
  // 所以我们为下个阶段 codegen 铺路,在 transform 指定 codegen 处理的节点
  root.codegenNode = root.children[0]
}

现在我们的模块职责就比较清楚了,codegen 只处理 codegenNode

至于 parse 的 ast 怎么变成 codegenNode,那就是 transform 的事情了