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

news2024/12/25 13:04:13

模板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')]);
// }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/175261.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件测试——测试用例

作者&#xff1a;~小明学编程 文章专栏&#xff1a;测试开发 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 测试用例的设计方法 等价类 边界值 错误猜测法 判定表法&#xff08;使用于关系组合&#xff09; 设计步骤 具体例子 正交法 场景设计法…

Redis相关简介

1. Redis 简介 在这个部分&#xff0c;我们将学习以下3个部分的内容&#xff0c;分别是&#xff1a; ◆ Redis 简介&#xff08;NoSQL概念、Redis概念&#xff09; ◆ Redis 的下载与安装 ◆ Redis 的基本操作 1.1 NoSQL概念 1.1.1 问题现象 在讲解NoSQL的概念之前呢&am…

8. R语言画:散点图、直方图、条形图、箱线图、小提琴图、韦恩图

b站课程视频链接&#xff1a; https://www.bilibili.com/video/BV19x411X7C6?p1 腾讯课堂(最新&#xff0c;但是要花钱&#xff0c;我花99&#x1f622;&#x1f622;元买了&#xff0c;感觉讲的没问题&#xff0c;就是知识点结构有点乱&#xff0c;有点废话&#xff09;&…

九大数据分析方法-综合型分析方法以及如何使用这九大分析方法

文章目录3 综合型分析方法3.1 相关性分析法3.1.1 直接相关3.1.2 间接相关3.2标签分析法3.3 MECE法4 如何使用九大方法本文来源&#xff0c;为接地气的陈老师的知识星球&#xff0c;以及付同学的观看笔记。3 综合型分析方法 3.1 相关性分析法 相关性分析法&#xff1a;寻找指标…

ROS2机器人编程简述humble-第二章-Executors .3.5

ROS2机器人编程简述humble-第二章-Parameters .3.4由于ROS2中的节点是C对象&#xff0c;因此一个进程可以有多个节点。事实上&#xff0c;在许多情况下&#xff0c;这样做是非常有益的&#xff0c;因为当通信处于同一进程中时&#xff0c;可以通过使用共享内存策略来加速通信。…

freeglut 在mfc 下的编译

freeglut 是OpenGL Utility Toolkit (GLUT) library 的替代版本&#xff0c;glut 应用广阔&#xff0c;但比较陈旧&#xff0c;很久没有更新。 我原来的opengl 用的是glut&#xff0c; 想更新到64位版本&#xff0c;怎么也找不到合适的下载。最后找到完全替代版本freeglut。fre…

【Linux】线程概念 | 互斥

千呼万唤始出来&#xff0c;终于到多线程方面的学习了&#xff01; 所用系统Centos7.6 本文的源码&#x1f449;【传送门】 最近主要是在我的hexo个人博客上更新&#xff0c;csdn的更新会滞后 文章目录1.线程的概念1.1 执行流1.2 线程创建时做了什么&#xff1f;1.3 内核源码中…

每刻和金蝶云星空接口打通对接实战

接通系统&#xff1a;每刻3000中大型企业在用&#xff0c;新一代业财税一体化解决方案提供商。旗下拥有每刻报销、每刻档案、每刻云票、每刻财务共享云平台等&#xff0c;助力企业实现财务数字化转型。对接系统&#xff1a;金蝶云星空金蝶K/3Cloud结合当今先进管理理论和数十万…

算法刷题打卡第72天:最少侧跳次数

最少侧跳次数 难度&#xff1a;中等 给你一个长度为 n 的 3 跑道道路 &#xff0c;它总共包含 n 1 个 点 &#xff0c;编号为 0 到 n 。一只青蛙从 0 号点第二条跑道 出发 &#xff0c;它想要跳到点 n 处。然而道路上可能有一些障碍。 给你一个长度为 n 1 的数组 obstacle…

Cheat Engine7.4 训练教程(非常详细)

目录 一.CE是干什么的&#xff1f; 二.怎么获得&#xff1f; 三.训练教程 步骤 1: 加载进程 步骤 2: 精确值扫描 步骤 3: 未知的初始值 步骤 4: 浮点数 步骤 5: 代码查找 步骤 6: 指针 步骤 7: 代码注入 步骤 8: 多级指针 提示&#xff1a;这篇文章不是一天写完的&a…

设计模式-建造者模式

1.概述 将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于&#xff1a;某个对象的构建过程复杂的情况。由于实现了构建和装配的解耦…

基于ffmpeg的视频处理与MPEG的压缩试验(下载安装使用全流程)

基于ffmpeg的视频处理与MPEG的压缩试验ffmpeg介绍与基础知识对提取到的图像进行处理RGB并转化为YUV对YUV进行DCT变换对每个8*8的图像块进行进行量化操作ffmpeg介绍与基础知识 ffmpeg是视频和图像处理的工具包&#xff0c;它的下载网址是https://ffmpeg.org/download.html。页面…

MySQL高级

存储引擎 MySQL体系结构&#xff1a; 存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表而不是基于库的&#xff0c;所以存储引擎也可以被称为表引擎。 默认存储引擎是InnoDB。 相关操作&#xff1a; -- 查询建表语句 show create table ac…

Python量化交易07——散户反买策略

参考来源&#xff1a;b站up 邢不行 我们都知道在A股&#xff0c;散户一直是最大的韭菜贡献组群。散户买入多的个股&#xff0c;大概率可能跌的很惨&#xff0c;散户卖出多的股票&#xff0c;大概率会涨。 跟着北向资金买能赚钱&#xff0c;那么跟着散户反买&#xff0c;是不是…

宝塔面板部署Django项目教程(手把手)

一、测试环境 系统&#xff1a;centos 7.9 CPU&#xff1a;1核 内存&#xff1a;2G 二、安装宝塔面板 输入命令&#xff1a; yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh ed8484bec 后面会…

【nvidia CUDA 高级编程】使用cub库优化分布式计算下的原子操作

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

MySQL数据库相关错题本

1) MySQL数据库相关错题本1、存储引擎相关1、MySql的存储引擎的不同MySQL存储引擎主要有InnoDB, MyISAM, Memory, 这三个区别在于:Memory是内存数据引擎, 会断电重启(在双M或者主从架构下会产生较多异常), 且不支持行级锁. 默认索引是数组索引, 支持B索引InnoDB和MyISAM的区别:…

【React全家桶】react简介(一)

react简介创建项目creat-react-app1.1 React特点1.2 引入文件1.3 JSX1.3.1 为什么要用JSX1.3.2 JSX语法规则1.4 虚拟DOM1.5 模块与组件1.5.1 模块React面向组件编程2.1 创建组件2.1.1 函数式组件2.1.2 类式组件2.2 组件实例的三大属性2.2.1 state属性2.2.2 props属性2.2.3 refs…

jvm学习的核心(三)---运行时数据区详解(2)

文章目录1.堆&#xff08;heap&#xff09;1.1 堆的概述1.2 堆的内部结构1.3 堆分代垃圾回收流程的简单理解2.方法区&#xff08; Method Area&#xff09;2.1 HotSpot方法区的演进2.2方法区的内部结构2.3.1 常量池和运行时常量池概念区别1.堆&#xff08;heap&#xff09; 1.1…

Linux常用命令——supervisord命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) supervisord 配置后台服务/常驻进程的进程管家工具 安装 # 安装 supervisord apt-get install supervisor实例 生成配置文件/etc/supervisord.conf [program:app] command/usr/bin/gunicorn -w 1 wsgiapp:ap…