Babel

使用

为什么需要@babel/runtime?它和@babel/polyfill有什么区别?

首先说下polyfill。babel默认只会转换语法,而不是API。比如说Set、Map这种,还是需要引入polyfill来兼容老的浏览器。其本质还是引用了core-js

@babel/runtime的使用场景是将一个babel转换时的功能函数进行了封装。 这里举个例子,假设我们要转化如下语法:

{ [name]: 'JavaScript' }

babel的转换结果为:

'use strict';
function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
var obj = _defineProperty({}, 'name', 'JavaScript');

这里的_defineProperty就是一个帮助函数,如果代码变得更加复杂,可能会有多个它存在与不同的模块。所以babel会提一个公共的包,也就是runtime,来做这些事情,将转换结果变为:

'use strict';
// The previous _defineProperty function has been used as a public module `babel-runtime/helpers/defineProperty'.
var _defineProperty2 = require('babel-runtime/helpers/defineProperty');
var _defineProperty3 = _interopRequireDefault(_defineProperty2);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var obj = (0, _defineProperty3.default)({}, 'name', 'JavaScript');

babel能解析html吗?为什么?

babel目前只能解析javascript,babel的解析器是@babel/parser(之前是babyion)。其是基于acorn,fork出来的一套parser。 babel的抽象语法树是基于estree拓展的一套Babel AST,所以只能解析js。

如果需要解析html,可以基于acorn的插件机制提供一个插件(@babel/parser未提供插件机制),也可以直接fork @babel/parser来,但这些成本都比较大,不如直接用现成的解析器:parse5

babel的预设presets和插件plugins的执行顺序是怎么样的?

首先需要理解babel的完整工作流程,babel的preset和plugins都是在transform阶段起作用。

preset就是一群plugins的集合。常见的几个可以参考Babel | Awesome-url

它们的执行顺序如下:

  1. 先执行 plugins 的配置项,再执行 Preset 的配置项;

  2. plugins 配置项,按照声明顺序执行

  3. Preset 配置项,按照声明逆序执行

有没有写过babel插件,是什么模式解析的?

自己做过比较简单的demo:babelDemo/easyPlugin at master · FunnyLiu/babelDemo

module.exports = function testPlugin(babel) {
    return {
      visitor: {
        Identifier(path) {
        // 将所有的foo改成bar
          if (path.node.name === 'foo') {
            path.node.name = 'bar';
          }
        }
      }
    };
  };
module.exports = function testPlugin(babel) {
  return {
    visitor: {
      Identifier(path) {
        // 将所有的bar2改成bar3
        if (path.node.name === "bar2") {
          path.node.name = "bar3";
        }
      },
      //将==变成===
      BinaryExpression(path) {
        if (path.node.operator == "==") {
          path.node.operator = "===";
        }
        // ...
      },
    },
  };
};

也做过一些特定AMD转ESM的插件。

这里的visitor其实就是具体访问者。而在解析AST的过程中,babel会将每一个符合type的语法树node,交给访问者队列来处理。比如Identifier就会顺序交给上面两个插件对应的方法来处理。之所以这样就为了将算法和真正的对象隔离,从而解耦方便扩展。

想象一下,Babel 有那么多插件,如果每个插件自己去遍历AST,对不同的节点进行不同的操作,维护自己的状态。这样子不仅低效,它们的逻辑分散在各处,会让整个系统变得难以理解和调试, 最后插件之间关系就纠缠不清,乱成一锅粥。

参考:

FunnyLiu/babel at readsource

design - 设计模式(以Typescript描述)

深入浅出 Babel 上篇:架构和原理 + 实战

babel在做polyfill时,如何保证不污染原型。

默认情况下,babel通过core-js来做api的polyfill,但是会污染各个对象的原型,想要不污染的话,需要配置@babel/plugin-transform-runtime和@babel/runtime-corejs3来完成。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "debug": true
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3 // 指定 runtime-corejs 的版本,目前有 2 3 两个版本
      }
    ]
  ]
}

他的作用就是将core-js原本修改原型的逻辑,改成自己实现一套函数。

比如下图这样,从而保证不污染Array的原型。

useBuiltIns 参数说明:

  • false: 不对 polyfills 做任何操作
  • entry: 根据 target 中浏览器版本的支持,将 polyfills 拆分引入,仅引入有浏览器不支持的 polyfill
  • usage(新):检测代码中 ES6/7/8 等的使用情况,仅仅加载代码中用到的 polyfills

原理

babel生态下有哪些包,分别在做什么?

参考源码阅读系列:FunnyLiu/babel at readsource

babel的完整工作流程

Babel 的功能很纯粹,它只是一个编译器。

Parse(解析):将源代码转换成更加抽象的表示方法(例如抽象语法树)

Transform(转换):对(抽象语法树)做一些特殊处理,让它符合编译器的期望(babel的插件就是在这个阶段起作用的)

Generate(代码生成):将第二步经过转换过的(抽象语法树)生成新的代码

接下来一步步介绍

parse解析

分为词法解析和语法解析。词法解析就是分词,将代码变成类似词语数组的形式。比如const add = (a, b) => a + b,就会变成:

[
    { "type": "Keyword", "value": "const" },
    { "type": "Identifier", "value": "add" },
    { "type": "Punctuator", "value": "=" },
    { "type": "Punctuator", "value": "(" },
    { "type": "Identifier", "value": "a" },
    { "type": "Punctuator", "value": "," },
    { "type": "Identifier", "value": "b" },
    { "type": "Punctuator", "value": ")" },
    { "type": "Punctuator", "value": "=>" },
    { "type": "Identifier", "value": "a" },
    { "type": "Punctuator", "value": "+" },
    { "type": "Identifier", "value": "b" }
]

语法解析是将词语数组变成AST。 上面那些词语数组对于的ASTjson文件

transform转换

我们编写的 babel 插件主要专注于这一步转换过程的工作。通过Babel提供的API,对AST的各个节点进行添加、更新或移除等操作。

Babel 自 6.0 起,就不再对代码进行转换。现在只负责图中的 parse 和 generate 流程,转换代码的 transform 过程全都交给插件去做。

Babel 对于 AST 的遍历是深度优先遍历,对于 AST 上的每一个分支 Babel 都会先向下遍历走到尽头,然后再向上遍历退出刚遍历过的节点,然后寻找下一个分支。

generate生成

经过了前面的步骤,现在的AST已经是最新的状态了,现在就需要根据AST来输出代码了。通过@babel/generator,将AST转换成js代码。

参考:

Babel 内部原理分析 | Tsung's BLOG

前端自习课

各插件工作原理

模板字符串怎么转?

参考@babel/plugin-transform-template-literals · Babel

通过String.prototype.concat来拼接字符串。

扩展运算符怎么转?

数组参考@babel/plugin-transform-spread · Babel

通过Array.prototype.concat来拼接数组。

对象参考@babel/plugin-proposal-object-rest-spread · Babel

利用Object.assign来模拟。

剩余参数怎么转?

参考@babel/plugin-transform-parameters · Babel

通过arguments来。

for-of怎么转?

参考@babel/plugin-transform-for-of · Babel

通过[Symbol.iterator]和其next方法来实现。不了解可迭代可以看看如何让一个对象变得可迭代,可迭代的本质是什么?

解构怎么转?

参考@babel/plugin-transform-destructuring · Babel

针对对象就是直接a.b.c,针对数组则是通过Array.prototype.slice来模拟。

块级作用域怎么转?

参考@babel/plugin-transform-block-scoping · Babel

将{}内的变量通过_a来命名,从而和外部的区分开来。

{
  let a = 3;
}

let a = 3;

function bbb() {
  
  let c ='1';
}

let c = '2';

if(true){
  let d = 1;
}
var d = 2;

变为:

"use strict";

{
  var _a = 3;
}
var a = 3;

function bbb() {
  var c = '1';
}

var c = '2';

if (true) {
  var _d = 1;
}

var d = 2;

箭头函数怎么转?

参考@babel/plugin-transform-arrow-functions · Babel

转为普通函数,this使用上一层作用域的this。

async怎么转?

参考@babel/plugin-transform-async-to-generator · Babel

转成generator

私有属性怎么转?

参考@babel/plugin-proposal-private-property-in-object · Babel

通过weakmap来存储私有属性,mock #操作符。

class和extends怎么转?

先了解怎么实现继承:简单实现继承

参考@babel/plugin-transform-classes · Babel

class是通过构造函数和prototype来完成:

class Test {
  constructor(name) {
    this.name = name;
  }

  logger() {
    console.log("Hello", this.name);
  }
}
// 转为:

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Test = (function() {
  function Test(name) {
    _classCallCheck(this, Test);

    this.name = name;
  }

  Test.prototype.logger = function logger() {
    console.log("Hello", this.name);
  };

  return Test;
})();

针对继承是通过封装一个方法,基本如下:

function extend(A, B) {
  function f() {}
  f.prototype = B.prototype;
  A.prototype = new f();
  A.prototype.constructor = A;
}

// 或者看看复杂版:

function _inherits(subClass, superClass) {
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true },
  });
  Object.defineProperty(subClass, "prototype", { writable: false });
  if (superClass) _setPrototypeOf(subClass, superClass);
}