[Vue] Vue 模板编译原理解析 part3

生成 JavaScript AST

我们要对整个模板的 AST 进行转换,转换为 JS AST。

我们目前的代码已经有了遍历模板 AST,并且针对不同的节点,做不同操作的能力。

我们首先需要知道 JS AST 长什么样子:

function render(){
  return null;
}

上面的代码,所对应的 JS AST 如下图所示:

image-20231120143716229

这里有几个比较关键的部分:

  • id:对应的是我函数的名称,类型为 Identifier
  • params:对应的是函数的参数,是一个数组的形式来表示的
  • body:对应的是函数体,由于函数体是可以有多条语句的,因此也是一个数组

我们仿造上面的设计,自己设计一个基本的数据结构来描述函数声明语句:

const FunctionDeclNode = {
  type: 'FunctionDecl', // 表示该节点是一个函数声明
  id: {
    type: 'Identifier',
    name: 'render', // 函数的名称
  },
  params: [],
  body: [
    {
      type: 'ReturnStatement',
      return: null
    }
  ]
}

回到我们上面的模板:

<div><p>Vue</p><p>React</p></div>

转换出来的渲染函数:

function render(){
  return h('div', [
    h('p', 'Vue'),
    h('p', 'React')
  ])
}

根据渲染函数所对应的 AST 去分析对应的节点

下面说一下几个比较重要的节点:

h 函数对应的节点:

const callExp = {
  type: 'CallExpression',
  callee: {
    type: 'Identifier',
    name: 'h'
  }
}

字符串所对应的节点:

const Str = {
  type: 'StringLiteral',
  value: 'div'
}

数组对应的节点:

const Arr = {
  type: 'ArrayExpression',
  elements: []
}

分析完节点之后,那么我们上面的那个 render 函数所对应的 AST 就应该长下面的样子:

{
  "type": "FunctionDecl",
  "id": {
      "type": "Identifier",
      "name": "render"
  },
  "params": [],
  "body": [
      {
          "type": "ReturnStatement",
          "return": {
              "type": "CallExpression",
              "callee": {"type": "Identifier", "name": "h"},
              "arguments": [
                  { "type": "StringLiteral", "value": "div"},
                  {"type": "ArrayExpression","elements": [
                        {
                            "type": "CallExpression",
                            "callee": {"type": "Identifier", "name": "h"},
                            "arguments": [
                                {"type": "StringLiteral", "value": "p"},
                                {"type": "StringLiteral", "value": "Vue"}
                            ]
                        },
                        {
                            "type": "CallExpression",
                            "callee": {"type": "Identifier", "name": "h"},
                            "arguments": [
                                {"type": "StringLiteral", "value": "p"},
                                {"type": "StringLiteral", "value": "React"}
                            ]
                        }
                    ]
                  }
              ]
          }
      }
  ]
}

分析完结构之后,下一步我们就是书写对应的转换函数。在转换函数之前,我们需要一些辅助函数,这些辅助函数用于帮助我们创建 JS AST 的节点:

function createStringLiteral(value) {
  return {
    type: "StringLiteral",
    value,
  };
}

function createIdentifier(name) {
  return {
    type: "Identifier",
    name,
  };
}

function createArrayExpression(elements) {
  return {
    type: "ArrayExpression",
    elements,
  };
}

function createCallExpression(callee, args) {
  return {
    type: "CallExpression",
    callee: createIdentifier(callee),
    arguments: args,
  };
}

接下来,我们就需要去修改我们的转换函数,一个有三个转换函数,分别是:

  • transformText
function transformText(node, context) {
  if (node.type !== "Text") return;
  node.jsNode = createStringLiteral(node.content);
}
  • transformElement
// 接下来我们就可以书写一些转换函数
// 将之前写在 traverseNode 里面的各种转换逻辑抽离出来了
function transformElement(node) {
  // 对外部返回一个函数,这个函数就是在退出节点时要执行的回调函数
  return () => {
    if (node.type !== "Element") return;
    // 1. 创建 h 函数的 AST 节点
    const callExp = createCallExpression("h", [
      createStringLiteral(node.tag),
    ]);
    // 2. 处理 h 函数里面的参数
    node.children.length === 1
      ? // 如果之后一个子节点,那么直接将子节点的 jsNode 作为参数即可
        callExp.arguments.push(node.children[0].jsNode)
      : // 如果是多个子节点,那么就需要将子节点的 jsNode 作为数组传入
        callExp.arguments.push(
          createArrayExpression(
            node.children.map((child) => child.jsNode)
          )
        );
  };
}
  • transformRoot
// 最后再写一个转换函数,负责转换 Root 根节点
function transformRoot(node) {
  return () => {
    if (node.type !== "Root") return;
    // 生成最外层的节点
    const vnodeJSAST = node.children[0].jsNode;
    node.jsNode = {
      type: "FunctionDecl",
      id: {
        type: "Identifier",
        name: "render",
      },
      params: [],
      body: [
        {
          type: "ReturnStatement",
          return: vnodeJSAST,
        },
      ],
    };
  };
}

最后在 transform 中使用这三个转换函数

生成器

整理一下思绪,哪怕你前面都没有听懂,但是你需要知道我们走到哪一步了。

目前我们已经有 js ast,只剩下最后一步,革命就成功了。

遍历这个生成的 js ast,转为具体的渲染函数

function compile(template){
  // 1. 得到模板的 AST
  const ast = parse(template)
  // 2. 将模板 AST 转为 JS AST
  transform(ast)
  // 3. 代码生成
  const code = genrate(ast.jsNode);
  
  return code;
}

和转换器一样,我们在生成器内部也需要维护一个上下文对象,为我们提供一些辅助函数和必要的信息:

// 和上一步转换器非常相似,我们也需要一个上下文对象
const context = {
  // 存储最终所生成的代码
  code: "",
  // 在生成代码的时候,通过调用 push 方法来进行拼接
  push(code) {
    context.code += code;
  },
  // 当前缩进的级别,初始值为 0,也就是没有缩进
  currentIndent: 0,
  // 该方法用来换行,会根据当前缩进的级别来添加相应的缩进
  newline() {
    context.code += "\n" + `  `.repeat(context.currentIndent);
  },
  // 用来缩进,会将缩进级别加一
  indent() {
    context.currentIndent++;
    context.newline();
  },
  // 用来取消缩进,会将缩进级别减一
  deIndent() {
    context.currentIndent--;
    context.newline();
  },
};

之后调用 genNode 方法,而 genNode 方法的内部,就是根据不同的 AST 节点类型,调用对应的生成方法:

function genNode(node, context) {
  // 我这里要做的事情,就是根据你当前节点的 type 来调用不同的方法
  switch (node.type) {
    case "FunctionDecl":
      genFunctionDecl(node, context);
      break;
    case "ReturnStatement":
      genReturnStatement(node, context);
      break;
    case "CallExpression":
      genCallExpression(node, context);
      break;
    case "StringLiteral":
      genStringLiteral(node, context);
      break;
    case "ArrayExpression":
      genArrayExpression(node, context);
      break;
  }
}

每一种生成方法本质都非常简单,就是做字符串的拼接:

// 之后我们要做的就是完善上面的各种生成方法,而每一种生成方法的实质其实就是做字符串的拼接

// 生成函数声明
function genFunctionDecl(node, context) {
  // 从上下文中获取一些实用函数
  const { push, indent, deIndent } = context;
  // 向输出中添加 "function 函数名"
  push(`function ${node.id.name} `);
  // 添加左括号开始参数列表
  push(`(`);
  // 生成参数列表
  genNodeList(node.params, context);
  // 添加右括号结束参数列表
  push(`) `);
  // 添加左花括号开始函数体
  push(`{`);
  // 缩进,为函数体的代码生成做准备
  indent();
  // 遍历函数体中的每个节点,生成相应的代码
  node.body.forEach((n) => genNode(n, context));
  // 减少缩进
  deIndent();
  // 添加右花括号结束函数体
  push(`}`);
}

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

// 生成 return 语句
function genReturnStatement(node, context) {
  const { push } = context;
  // 添加 "return "
  push(`return `);
  // 生成 return 语句后面的代码
  genNode(node.return, context);
}

// 生成函数调用表达式
function genCallExpression(node, context) {
  const { push } = context;
  const { callee, arguments: args } = node;

  // 添加 "函数名("
  push(`${callee.name}(`);
  // 生成参数列表
  genNodeList(args, context);
  // 添加 ")"
  push(`)`);
}

// 生成字符串字面量
function genStringLiteral(node, context) {
  const { push } = context;
  // 添加 "'字符串值'"
  push(`'${node.value}'`);
}

// 生成数组表达式
function genArrayExpression(node, context) {
  const { push } = context;
  // 添加 "["
  push("[");
  // 生成数组元素
  genNodeList(node.elements, context);
  // 添加 "]"
  push("]");
}
posted @ 2025-03-30 22:12  Zhentiw  阅读(16)  评论(0)    收藏  举报