40 - codegen 实现三种类型联合

102 阅读2分钟

测试用例

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

最终生成

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

实现

目前问题

上节实现的 element类型转换,只是简单的判断如果是 element 类型就push对应的 helpertag,并没有考虑标签内包裹的内容,即转换成 ast 里的 children

处理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)) }

快照跟我们预期的任有所差别,字符串部分与插值部分没有用 + 拼接 如果是 textinterpolation 相邻的,我们做拼接处理,这一块单独处理的判断,可以加多一个 NodeType 来判断这种类型

新增联合类型

主要是处理 textinterpolation 相邻的情况

添加 COMPOUND_EXPRESSION 类型

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

添加 plugin

新增 transformText plugin ,主要处理 COMPOUND_EXPRESSION 类型数据

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-10 10:45:42
 * @LastEditTime: 2022-04-23 11:45:43
 * @LastEditors: Lin ZeFan
 * @Description:
 * @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformText.ts
 *
 */
import { NodeType } from "../ast";
import { isText } from "../utils";

export function transformText(node) {
  if (node.type === NodeType.ELEMENT) {
      // hi,{{msg}}
      // 上面的模块会生成2个节点,一个是 text 一个是 interpolation 的话
      // 生成的 render 函数应该为 "hi," + _toDisplayString(_ctx.msg)
      // 这里面就会涉及到添加一个 “+” 操作符
      // 那这里的逻辑就是处理它

      // 检测下一个节点是不是 text 类型,如果是的话, 那么会创建一个 COMPOUND 类型
      // COMPOUND 类型把 2个 text || interpolation 包裹(相当于是父级容器)

      const { children = [] } = node;
      let currentContainer;

      for (let i = 0; i < children.length; i++) {
        const child = children[i];

        /**
         * 1. 判断当前节点是否 text 类型,是的话创建一个 COMPOUND 类型
         * 2. 判断相邻元素是否为 text 类型,是的话通过 + 拼接
         */
        if (isText(child)) {
          // 看看下一个节点是不是 text 类
          for (let j = i + 1; j < children.length; j++) {
            const next = children[j];
            if (isText(next)) {
              if (!currentContainer) {
                // 相邻元素为text类型,把它们放到同一个children里
                currentContainer = children[i] = {
                  type: NodeType.COMPOUND_EXPRESSION,
                  children: [child],
                };
              }
              // 前面的元素为当前元素,这里再追加一个 + 拼接后一个元素
              currentContainer.children.push(` + `, next);
              // 把当前的节点放到容器内, 然后删除当前节点
              children.splice(j, 1);
              // 因为把 j 删除了,所以这里就少了一个元素,那么 j 需要 --
              j--;
            }
          }
        } else {
          currentContainer = undefined;
        }
      }
    };
}

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()
})

处理 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 (typeof child === "string") {
      // 直接 push
      push(child)
    } else {
      // 否则还是走 genNode
      genNode(child, context)
    }
  }
}

优化

优化 genElement

前面实现 genElement用了比较偷懒的形式,在扩展性上并不是特别好,针对element类型我们通过拓展transformElement plugin优化

拓展 transformElement plugin

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-10 10:45:42
 * @LastEditTime: 2022-04-23 12:30:13
 * @LastEditors: Lin ZeFan
 * @Description:
 * @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformElement.ts
 *
 */
import { NodeType } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";

export function transformElement(node, context) {
  if (node.type === NodeType.ELEMENT) {
    // 添加相关的helper
    context.helper(CREATE_ELEMENT_VNODE);

    // 中间处理层,处理 props 和 tag
    const { tag, props, children } = node;
    const vnodeTag = `'${tag}'`;
    const vnodeProps = props;
    const vnodeChildren = children;

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

    node.codegenNode = vnodeElement;
  }
}

新增 createRootCodegen 内部判断

// transform.ts

function createdRootCodegen(root: any) {
  const { children } = root;
  for (let index = 0; index < children.length; index++) {
    const child = children[index];
    // 在这里进行判断,如果说 child 的类型是 ELEMENT,那么直接取child.codegenNode
    if (child.type === NodeType.ELEMENT) {
      root.codegenNode = child.codegenNode;
    } else {
      root.codegenNode = child;
    }
  }
}

修改 genElement

function genElement(node, context) {
  const { push, helper } = context;
  const { tag, children, props } = node;
  push(`${helper(CREATE_ELEMENT_VNODE)}(`);
  // 批量处理 tag,props 和 children,优化空值情况
  genNodeList(genNullable([tag, props, children]), context);
  // 每创建一个 createVNode,就要加上一个结束符号
  push(`)`);
}

function genNodeList(nodes, context) {
  const { push } = context;
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    /** 处理node list
     * 1. 如果是text,直接拼接
     * 2. 如果是数组,遍历数组,把每一项再通过 genNode 检测类型
     * 3. 如果是对象,给 genNode 检测类型
     */
    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);
    }
    // 遍历完,加上分隔符
    i < nodes.length - 1 && push(", ");
  }
}

function genNullable(args) {
  // 把undefined、null,转为 “null”
  return args.map((arg) => arg || "null");
}

优化插件执行顺序

实现完 genElement 优化,产生了另外一个问题,就是 interpolation 的 ctx 消失了?

出现这个问题的原因,在于我们通过 transformText 这个 plugin 去处理 相邻元素的一些操作,改变了vnode的结构,把插值类型包裹到复合类型里面去了。

解决这个问题关键是,让 transformExpression 这个 plugin 优先执行,也就是优化插件执行顺序

方案:

  1. 在遍历我们的nodeTransforms里的plugin时,收集所有plugin
  2. 遍历完所有plugin后,再去执行收集的plugin,若是返回一个
  • 插件执行返回的是一个函数,表示该插件会是一个退出执行函数
  • 始执行退出函数
// transform.ts

function traverseNode(node, context) {
  const { nodeTransforms } = context;
  const exitFns: any = [];
  for (let index = 0; index < nodeTransforms.length; index++) {
    const transform = nodeTransforms[index];
    const exitFn = transform(node, context);
    // 收集退出函数,即返回一个函数的plugin
    // 这里只有 transformElement、transformText 两个plugin做了退出函数,因为要优先执行 transformExpression
    if (exitFn) exitFns.push(exitFn);
  }

  // 在这里遍历整棵树的时候,将根据不同的 node 的类型存入不同的 helper
  switch (node.type) {
    case NodeType.INTERPOLATION:
      context.helper(TO_DISPLAY_STRING);
      break;
    case NodeType.ROOT:
    case NodeType.ELEMENT:
      // 只有在 ROOT 和 ELEMENT 才会存在 children
      traverseChildren(node, context);
      break;
    default:
      break;
  }

  let i = exitFns.length;
  // 执行所有的退出函数
  while (i--) {
    typeof exitFns[i] === "function" && exitFns[i]();
  }
}

修改 transformText plugin

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-10 10:45:42
 * @LastEditTime: 2022-04-23 11:45:43
 * @LastEditors: Lin ZeFan
 * @Description:
 * @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformText.ts
 *
 */
import { NodeType } from "../ast";
import { isText } from "../utils";

export function transformText(node) {
  if (node.type === NodeType.ELEMENT) {
    // 在 exit 的时期执行
    // 下面的逻辑会改变 ast 树
    // 有些逻辑是需要在改变之前做处理的
    return () => {
      // hi,{{msg}}
      // 上面的模块会生成2个节点,一个是 text 一个是 interpolation 的话
      // 生成的 render 函数应该为 "hi," + _toDisplayString(_ctx.msg)
      // 这里面就会涉及到添加一个 “+” 操作符
      // 那这里的逻辑就是处理它

      // 检测下一个节点是不是 text 类型,如果是的话, 那么会创建一个 COMPOUND 类型
      // COMPOUND 类型把 2个 text || interpolation 包裹(相当于是父级容器)

      const { children = [] } = node;
      let currentContainer;

      for (let i = 0; i < children.length; i++) {
        const child = children[i];

        /**
         * 1. 判断当前节点是否 text 类型,是的话创建一个 COMPOUND 类型
         * 2. 判断相邻元素是否为 text 类型,是的话通过 + 拼接
         */
        if (isText(child)) {
          // 看看下一个节点是不是 text 类
          for (let j = i + 1; j < children.length; j++) {
            const next = children[j];
            if (isText(next)) {
              if (!currentContainer) {
                // 相邻元素为text类型,把它们放到同一个children里
                currentContainer = children[i] = {
                  type: NodeType.COMPOUND_EXPRESSION,
                  children: [child],
                };
              }
              // 前面的元素为当前元素,这里再追加一个 + 拼接后一个元素
              currentContainer.children.push(` + `, next);
              // 把当前的节点放到容器内, 然后删除当前节点
              children.splice(j, 1);
              // 因为把 j 删除了,所以这里就少了一个元素,那么 j 需要 --
              j--;
            }
          }
        } else {
          currentContainer = undefined;
        }
      }
    };
  }
}

修改transformElement plugin

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-10 10:45:42
 * @LastEditTime: 2022-04-23 14:06:34
 * @LastEditors: Lin ZeFan
 * @Description:
 * @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformElement.ts
 *
 */
import { NodeType } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";

export function transformElement(node, context) {
  if (node.type === NodeType.ELEMENT) {
    // 返回一个函数
    return () => {
      // 添加相关的helper
      context.helper(CREATE_ELEMENT_VNODE);

      // 中间处理层,处理 props 和 tag
      const { tag, props, children } = node;
      const vnodeTag = `'${tag}'`;
      const vnodeProps = props;
      const vnodeChildren = children;

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

      node.codegenNode = vnodeElement;
    };
  }
}

最后去修改我们的测试用例

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

图示

image.png

我们初始化过程应该是从 transformExpression - transformText - transformElement

所以例子上,plugin的顺序才这么放