[Vue] TemplateAST to JsAST

我们现在已经有了模板AST,那么根据之前的顺序,我们现在需要把模板AST转化为JSAST。

要知道,模板AST是对模板的描述,那么JSAST就是对JS代码的描述。

也就是说,我们最终希望得到的代码是下面这样的:

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

JSAST,其实就是对这段代码的描述,而这个描述,应该是从模板AST转换而来的

所以本质上,我们首先应该知道,上面这段JS代码,应该怎么被描述,简单来说,我们应该设计一些数据结构来描述渲染函数的代码。最终,我们只需要在转换函数中,将模板AST,依据我们需要的描述就行转换,就形成了JSAST。

渲染函数的描述

function render(){
  return ...
}

根据函数声明语句,我们可以确定以下的特点

  • id:函数名称,它是一个标识符Identifier
  • params:函数参数,它是一个数组
  • body:函数体,由于函数体可以包含多个语句,因此它也是一个数组

所以,一个针对render函数最简单的数据结构描述,可以声明成下面这个样子

const FunctionDeclNode = {
  type: 'FunctionDecl' // 代表该节点是函数声明
  id: {
  	type: 'Identifier',
  	name: 'render' //渲染函数名称标识
	},
  params:[], // 参数
  body:[
    {
      type: 'ReturnStatement' // 返回
      return: null // 暂时设置为null
    }
  ]
}

而具体的函数体中,首先就是一个h函数的调用

h(......)

我们使用下面的形式描述函数调用语句:

const CallExp = {
  type: 'CallExpression',
  callee: {
    type: 'Identifier', // 被调用函数的名称标识符
    name: 'h'
  },
  arguments: [] // 参数
}

类型为CallExpression的节点拥有两个属性

  • callee:描述被调用函数的名称,本身是一个标识符节点
  • arguments:被调用函数的形参,多个参数使用数组来描述

而,具体h函数的参数,是这样的:

h('div',[/*......*/])

第一个参数就是一个字符串字面量,我们可以定义描述的数据结构

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

第二个参数是一个数组, 同样定义描述的数据结构

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

最终,我们把描述结构结合到一起:

// 最终的render函数
function render(){
  return h('div',[
    h('p','Vue'),
    h('p','Template')
  ])
}


const FunctionDeclNode = {
  type: "FunctionDecl", // 代表该节点是函数声明
  // 函数的名称是一个标识符,标识符本身也是一个节点
  id: {
    type: "Identifier",
    name: "render",
  },
  params: [], // 参数
  // 渲染函数的函数体只有一个return 语句
  // body 是一个数组,一个函数体可以包含多个语句,每个语句都是一个节点
  body: [
    {
      type: "ReturnStatement",
      // 最外层的 h 函数调用
      return: {
        type: "CallExpression",
        callee: { type: "Identifier", name: "h" },
        arguments: [
          // 第一个参数是字符串字面量 'div'
          {
            type: "StringLiteral",
            value: "div",
          },
          // 第二个参数是一个数组
          {
            type: "ArrayExpression",
            elements: [
              // 数组的第一个元素是 h 函数的调用
              {
                type: "CallExpression",
                callee: { type: "Identifier", name: "h" },
                arguments: [
                  // 该 h 函数调用的第一个参数是字符串字面量
                  { type: "StringLiteral", value: "p" },
                  // 第二个参数也是一个字符串字面量
                  { type: "StringLiteral", value: "Vue" },
                ],
              },
              // 数组的第二个元素也是 h 函数的调用
              {
                type: "CallExpression",
                callee: { type: "Identifier", name: "h" },
                arguments: [
                  { type: "StringLiteral", value: "p" },
                  { type: "StringLiteral", value: "Template" },
                ],
              },
            ],
          },
        ],
      },
    },
  ],
};

所以,我们现在任务,就是编写转换函数,将模板AST转换为上面的JSAST描述

首先编写一些辅助函数用来创建上面说到的字符串字面量节点(StringLiteral),标识符节点(Identifier),数组表达式节点(ArrayExpression),函数调用表达式节点(CallExpression)

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

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

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

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

接下来,当然就是在转换函数上做做文章了。再次去改写我们之前已经写过的transformTexttransformElement函数

function transformElement(node) {
  return () => {
    if (node.type !== "Element") {
      return;
    }

    // 1.创建h函数调用语句
    const callExp = createCallExpression("h", [
      createStringLiteral(node.tag),
    ]);

    // 2.处理h函数调用参数
    node.children.length === 1
      // 如果只有一个子节点,直接传入子节点的jsNode作为参数
      ? callExp.arguments.push(node.children[0].jsNode)
      // 如果有多个子节点,创建一个ArrayExpression节点作为参数
      : callExp.arguments.push(
          createArrayExpression(node.children.map((c) => c.jsNode))
        );

    // 3.将当前标签节点对应的JSAST添加到jsNode属性上
    node.jsNode = callExp;
  };
}

function transformText(node, context) {
  if (node.type !== "Text") {
    return;
  }

  node.jsNode = createStringLiteral(node.content);
}

当上面这两个只是用来描述渲染函数render的返回值的,所以我们还需要把render函数本身的函数声明语句节点附加到JSAST中

function transformRoot(node) {
  return () => {
    // 如果不是root节点,直接返回
    if (node.type !== "Root") {
      return;
    }

    // node.children[0]是根节点的第一个子节点
    // 不考虑多个根节点的情况
    const vnodeJSAST = node.children[0].jsNode;

    // 创建render函数的声明语句节点
    // 将vnodeJSAST作为render函数的返回值
    node.jsNode = {
      type: "FunctionDecl",
      id: { type: "Identifier", name: "render" },
      params: [],
      body: [
        {
          type: "ReturnStatement",
          return: vnodeJSAST,
        },
      ],
    };
  };
}

当然,我们在transform函数中进行调用即可:

function transform(ast) {
    const context = {
      //......
      nodeTransforms: [transformRoot, transformElement, transformText],
    };
    //......
  }
posted @ 2025-06-27 13:34  Zhentiw  阅读(10)  评论(0)    收藏  举报