深入学习Vue.js(十二)编译器

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

模板DSL的编译器

1.编译器概述

编译器实际上是一段程序他用来将一种语言A翻译为另一种语言B。其中A被称为源代码B被称为目标代码。编译器将源代码翻译为目标代码的过程被称为编译。完整的编译过程通常包含词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等步骤。而整个编译过程通常分为编译前端和编译后端其中编译前端包括词法分析、语法分析和语义分析编译前端通常与目标平台无关仅负分析源代码。编译后端则通常与平台有关设计中间打吗生成、优化和目标代码生成。但是编译后端不一定会带有中间代码和优化这两个环节这取决于具体实现。因此中间代码生成和优化通常也被称为“中端”。

对于Vue来说源代码就是组件模板目标代码就是能在浏览器平台上运行的JavaScript代码。而Vue模板编译器的目标代码实际上就是渲染函数。Vue编译模板会首先对模板进行词法分析和语法分析得到模板AST。接着将模板AST转换为JavaScript AST。最后根据JavaScript AST生成目标JavaScript代码即渲染函数代码。

2.Vue.js编译器的组成

  • 用来将模板字符串解析为模板 AST 的解析器parser
  • 用来将模板 AST 转换为 JavaScript AST 的转换器 transformer
  • 用来根据 JavaScript AST 生成渲染函数代码的生成器 generator。

parser的实现原理

1.有限状态自动机

parser的入参是字符串模板解析器会逐个读取字符串模板中的字符然后根据一定的规则将这些字符切割为一个一个的token。假设对于这样一段模板

<p>
    TEXT
</p>

它会被切割为’<p>‘、‘TEXt’和’</p>’。而这个切割的过程则是使用有限状态机实现的。

image-20230121131732479

代码实现

code

现在运行一下上面的代码会得到下面的结果

const res = tokenize(`<p>Text</p>`);
console.log(res);
// [
//   { type: 'tag', name: 'p' },
//   { type: 'text', content: 'Text' },
//   { type: 'tagEnd', name: 'p' }
// ]

2.构建AST

构建AST这个过程主要是借助一个栈来实现的首先将根节点压入栈中当碰到起始标签就将起始标签推入栈中。每次有新元素的时候他的父节点都是当前栈顶元素。当碰到结束标签时就将当前栈顶元素弹出即可。但是目前这只是最基本实现只能生成典型模板的AST对于一些自闭合标签和语法不完整的模板是暂时没有处理能力的。

code

3.AST转换

接下来就是第二步AST转换也就是将模板AST转换为JavaScript AST。首先我们封装一个工具函数用于打印当前节点信息

code

code

然后我们定义一个traverseNode函数该函数用来对模板AST进行遍历转换操作。该函数可以通过传入配置参数的方式指定转换规则。

code

4.转换上下文与节点操作

我们在传入遍历AST的配置项中再加入几项参数当前转换节点、当前转换节点的父节点当前结点在父节点children中的索引、节点的替换和移除方法。

code

5.进入与退出

可以添加一个栈使有先进后出的函数能够按照栈的结构执行。

code

将模板AST转换为JavaScript AST

JavaScript AST的基本结构

<div><p>Vue</p><p>Template</p></div>

对于以上模板它对应的JavaScript AST结构为

const FunctionDeclNode = {
  type: 'FunctionDecl', // 代表该节点是函数声明
  // 函数的名称是一个标识符标识符本身也是一个节点
  id: {
    type: 'Identifier',
    name: 'render', // name 用来存储标识符的名称在这里它就是渲染函数的
  },
  params: [], // 参数目前渲染函数还不需要参数所以这里是一个空数组
  // 渲染函数的函数体只有一个语句即 return 语句
  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: [
                  // 该 h 函数调用的第一个参数是字符串字面量
                  { type: 'StringLiteral', value: 'p' },
                  // 第二个参数也是一个字符串字面量
                  { type: 'StringLiteral', value: 'Template' },
                ],
              },
            ],
          },
        ],
      },
    },
  ],
};

以及一些辅助创建AST的工具函数

// 用来创建 StringLiteral 节点
function createStringLiteral(value) {
  return {
    type: 'StringLiteral',
    value,
  };
}
// 用来创建 Identifier 节点
function createIdentifier(name) {
  return {
    type: 'Identifier',
    name,
  };
}
// 用来创建 ArrayExpression 节点
function createArrayExpression(elements) {
  return {
    type: 'ArrayExpression',
    elements,
  };
}
// 用来创建 CallExpression 节点
function createCallExpression(callee, arguments) {
  return {
    type: 'CallExpression',
    callee: createIdentifier(callee),
    arguments,
  };
}

下面使用刚才编写好的几个JavaScript AST属性创建建函数进行标签转换。

// 转换 Root 根节点
function transformRoot(node) {
  // 将逻辑编写在退出阶段的回调函数中保证子节点全部被处理完毕
  return () => {
    // 如果不是根节点则什么都不做
    if (node.type !== 'Root') {
      return;
    }
    // node 是根节点根节点的第一个子节点就是模板的根节点
    const vnodeJSAST = node.children[0].jsNode;
    // 创建 render 函数的声明语句节点将 vnodeJSAST 作为 render 函数的参数
    node.jsNode = {
      type: 'FunctionDecl',
      id: { type: 'Identifier', name: 'render' },
      params: [],
      body: [
        {
          type: 'ReturnStatement',
          return: vnodeJSAST,
        },
      ],
    };
  };
}

const transformElement = (node, context) => {
  return () => {
    if (node.type !== 'Element') {
      return;
    }

    const callExp = createCallExpression('h', [createStringLiteral(node.tag)]);

    node.children.length === 1
      ? callExp.arguments.push(node.children[0].jsNode)
      : callExp.arguments.push(createArrayExpression(node.children.map((c) => c.jsNode)));
    node.jsNode = callExp;
  };
};

const transformText = (node, context) => {
  if (node.type !== 'Text') {
    return;
  }
  node.jsNode = createStringLiteral(node.content);
};

目标代码生成

最后生成目标代码的代码实现如下

const parse = require('./parse');
const transform = require('./transform');
const { createArrayExpression, createCallExpression, createIdentifier, createStringLiteral } = './h.js';

function compile(template) {
  const ast = parse(template);
  transform(ast);
  const code = generate(ast.jsNode);
  return code;
}

function generate(node) {
  const context = {
    code: '',
    push(code) {
      context.code += code;
    },
    // 当前缩进的级别初始值为 0即没有缩进
    currentIndent: 0,
    // 该函数用来换行即在代码字符串的后面追加 \n 字符
    // 另外换行时应该保留缩进所以我们还要追加 currentIndent * 2 个空字符
    newline() {
      context.code += '\n' + ` `.repeat(context.currentIndent);
    },
    // 用来缩进即让 currentIndent 自增后调用换行函数
    indent() {
      context.currentIndent++;
      context.newline();
    },
    // 取消缩进即让 currentIndent 自减后调用换行函数
    deIndent() {
      context.currentIndent--;
      context.newline();
    },
  };

  genNode(node, context);

  return context.code;
}

function genNode(node, context) {
  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) {
  // 从 context 对象中取出工具函数
  const { push, indent, deIndent } = context;
  // node.id 是一个标识符用来描述函数的名称即 node.id.name
  push(`function ${node.id.name} `);
  push(`(`);
  // 调用 genNodeList 为函数的参数生成代码
  genNodeList(node.params, context);
  push(`) `);
  push(`{`);
  // 缩进
  indent();
  // 为函数体生成代码这里递归地调用了 genNode 函数
  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(', ');
    }
  }
}

function genArrayExpression(node, context) {
  const { push } = context;
  // 追加方括号
  push('[');
  // 调用 genNodeList 为数组元素生成代码
  genNodeList(node.elements, context);
  // 补全方括号
  push(']');
}

function genReturnStatement(node, context) {
  const { push } = context;
  // 追加 return 关键字和空格
  push(`return `);
  // 调用 genNode 函数递归地生成返回值代码
  genNode(node.return, context);
}

function genStringLiteral(node, context) {
  const { push } = context;
  // 对于字符串字面量只需要追加与 node.value 对应的字符串即可
  push(`'${node.value}'`);
}

function genCallExpression(node, context) {
  const { push } = context;
  // 取得被调用函数名称和参数列表
  const { callee, arguments: args } = node;
  // 生成函数调用代码
  push(`${callee.name}(`);
  // 调用 genNodeList 生成参数代码
  genNodeList(args, context);
  // 补全括号
  push(`)`);
}

const ast = parse(`<div><p>Vue</p><p>Template</p></div>`);
transform(ast);
const code = generate(ast.jsNode);
// function render() {
//   return h('div', [h('p', 'Vue'), h('p', 'Template')]);
// }
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue