Appearance
带你玩转 babel 工具链(五)彻底理解@babel/helpers 与 @babel/runtime 
一、前言 
前面我们学习了babel插件, 下面我们详细了解下比较黑盒的@babel/helpers 与 @babel/runtime, 虽然很少用到这个库,但他在 babel 工具链中也扮演了重要角色!
往期回顾:
二、babel 的运行时 helper 
什么叫babel的运行时helper? 我们可以拆分成两个词
- 运行时:顾名思义,就是代码运行的时候。
- helper: 是工具的意思
再结合 babel 的环境,合并起来的意思就是:babel 通过 AST 操作,为我们的代码中插入了helper工具函数。
那么在产物代码中,执行的时候就会用到这些工具函数,也就是运行时。
好的,了解了运行时 helper 的概念,我们继续看~
在日常项目中,我们通常能看到这样的产物, 我们使用了async语法,babel 自动为我们的代码中注入了_asyncToGenerator和asyncGeneratorStep方法。
.babelrc配置如下
js
{
  "presets": [
    "@babel/preset-env"
  ]
}{
  "presets": [
    "@babel/preset-env"
  ]
}如果我们不想要这些helper, 我们就不配置@babel/preset-env, 再重新打包
js
{ "presets": [] }{ "presets": [] }可以看到产物中已经没有了helper, 没有发生任何变化
那如果我们就是想要注入 helper, 但不想引入@babel/preset-env该如何实现呢?
我们来写一个babel插件
js
{
  "presets": [
  ],
  "plugins": [
    ["./src/plugins/index.js", { // 此处配置插件
      "a": 1
    }]
  ]
}{
  "presets": [
  ],
  "plugins": [
    ["./src/plugins/index.js", { // 此处配置插件
      "a": 1
    }]
  ]
}js
module.exports = function (api, options, dirname) {
  return {
    pre(file) {
      this.addHelper("asyncToGenerator"); // 关键代码
    },
    visitor: {},
  };
};module.exports = function (api, options, dirname) {
  return {
    pre(file) {
      this.addHelper("asyncToGenerator"); // 关键代码
    },
    visitor: {},
  };
};我们在pre函数中,调用addHelper添加了一个helper, 再重新打包看看~
可以看到,babel 帮我们自动注入了asyncGeneratorStep和_asyncToGenerator, 虽然 babel 没有帮我们把fn函数进行转换, 但是也实现了运行时 helper 的注入。
所以,问题又来,helpers的方法在哪里?
@babel/helpers 
我们可以找到@babel/helpers下的node_modules/@babel/helpers/lib/helpers.js, 可以发现我们的helper都是通过字符串形式创建的(借助@babel/template可以把字符串代码转成 AST)
感兴趣的话,大家可以打开这个文件,里面有很多helper的实现。
@babel/helpers 的缺点 
@babel/helpers 也是有些缺点的,通过前面的演示,可以发现,helper 是直接写在我们代码里的,我们先看个例子
有两个文件,都是用了async语法,按道理使用一份helper就可以了,但是最后却产生了两份!
js
// index.js
import test from "./test";
async function fn() {
  await test();
}
// test.js
export default async function () {
  return 1;
}// index.js
import test from "./test";
async function fn() {
  await test();
}
// test.js
export default async function () {
  return 1;
}那么怎么能解决上述问题呢?其实需要另外两个包来解决这种问题
- @babel/plugin-transform-runtime
- @babel/runtime
下面我将详细介绍@babel/runtime, 帮助大家了解这个黑盒。至于@babel/plugin-transform-runtime 后续将用单独的文章讲解。
三、@babel/runtime 到底是什么? 
首先我会在代码中切换两种配置
js
// @babel/helpers
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ],
  "plugins": [
  ]
}
// @babel/runtime
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ],
  "plugins": [
      "@babel/plugin-transform-runtime"
  ]
}// @babel/helpers
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ],
  "plugins": [
  ]
}
// @babel/runtime
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ],
  "plugins": [
      "@babel/plugin-transform-runtime"
  ]
}注意:只有配置
@babel/plugin-transform-runtime, 才会使用@babel/runtime。
我们对比一下@babel/helpers和@babel/runtime
代码对比:
产物对比:
总结一下有什么区别:
- 代码实现方面:@babel/runtime采用了模块化,每个方法单独导出,而@babel/helpers采用单独的文件导出。
- 产物方面:@babel/runtime的 helper 是模块化加载的,不会造成代码冗余。@babel/helpers是直接把代码注入到了项目代码里,会造成冗余。
但是,是不是以后项目都使用@babel/runtime + @babel/plugin-transform-runtime这种组合呢? 其实是不推荐的,为什么呢?我们接着看。
我们先了解一个知识点,@babel/plugin-transform-runtime它提供了什么功能:
关于
@babel/plugin-transform-runtime的详细讲解,请参考我的另一篇文章:编写中~
- helper使用模块化加载
- polyfill不污染全局,通过导出变量的形式引入,而不是直接覆盖全局方法的实现
由于@babel/plugin-transform-runtime并不支持targers配置。也就是说,所有的比较新的语言特性,都会被polyfill, 明明浏览器已经支持的功能,却还被polyfill,这显然是不合理的。所以我更推荐在开发lib库的时候使用@babel/plugin-transform-runtime。
那造成这种问题的原因是什么呢,我不是已经配置了@babel/preset-env的targets吗?
再来回顾下plugins和presets的执行顺序:
可以发现@babel/plugin-transform-runtime是优先执行的, 如果代码已经被转换过了,到达@babel/preset-env处理的时候,就没有效果了。
四、helper 是如何注入的 
在前面,我们提到过,使用addHelper可以插入一段运行时Helper
js
module.exports = function (api, options, dirname) {
  return {
    pre(file) {
      this.addHelper("asyncToGenerator"); // 关键代码
    },
    visitor: {},
  };
};module.exports = function (api, options, dirname) {
  return {
    pre(file) {
      this.addHelper("asyncToGenerator"); // 关键代码
    },
    visitor: {},
  };
};下面我们来具体分析:
@babel/helper 的注入逻辑 
首先我们定位到一段源码node_modules/@babel/core/lib/transformation/file/file.js
js
addHelper(name) {
    const declar = this.declarations[name];
    if (declar) return cloneNode(declar);
    const generator = this.get("helperGenerator");
    // transform-runtime 在此处拦截 实现了自己的generator
    if (generator) {
      const res = generator(name);
      if (res) return res;
    }
    // 加载helper对应的
    helpers().ensure(name, File);
    const uid = this.declarations[name] = this.scope.generateUidIdentifier(name);
    const dependencies = {};
    for (const dep of helpers().getDependencies(name)) {
      dependencies[dep] = this.addHelper(dep);
    }
    const {
      nodes,
      globals
    } = helpers().get(name, dep => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()));
    globals.forEach(name => {
      if (this.path.scope.hasBinding(name, true)) {
        this.path.scope.rename(name);
      }
    });
    nodes.forEach(node => {
      node._compact = true;
    });
    this.path.unshiftContainer("body", nodes);
    this.path.get("body").forEach(path => {
      if (nodes.indexOf(path.node) === -1) return;
      if (path.isVariableDeclaration()) this.scope.registerDeclaration(path);
    });
    return uid;
  }addHelper(name) {
    const declar = this.declarations[name];
    if (declar) return cloneNode(declar);
    const generator = this.get("helperGenerator");
    // transform-runtime 在此处拦截 实现了自己的generator
    if (generator) {
      const res = generator(name);
      if (res) return res;
    }
    // 加载helper对应的
    helpers().ensure(name, File);
    const uid = this.declarations[name] = this.scope.generateUidIdentifier(name);
    const dependencies = {};
    for (const dep of helpers().getDependencies(name)) {
      dependencies[dep] = this.addHelper(dep);
    }
    const {
      nodes,
      globals
    } = helpers().get(name, dep => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()));
    globals.forEach(name => {
      if (this.path.scope.hasBinding(name, true)) {
        this.path.scope.rename(name);
      }
    });
    nodes.forEach(node => {
      node._compact = true;
    });
    this.path.unshiftContainer("body", nodes);
    this.path.get("body").forEach(path => {
      if (nodes.indexOf(path.node) === -1) return;
      if (path.isVariableDeclaration()) this.scope.registerDeclaration(path);
    });
    return uid;
  }其中有几段逻辑
- 获取 helperGenerator 方法,如果存在就调用该方法,不会走后面逻辑,这也是 - @babel/plugin-transform-runtime实现- helper的地方,替换原有逻辑js- const declar = this.declarations[name]; if (declar) return cloneNode(declar); const generator = this.get("helperGenerator"); // transform-runtime 在此处拦截 实现了自己的generator if (generator) { const res = generator(name); if (res) return res; }- const declar = this.declarations[name]; if (declar) return cloneNode(declar); const generator = this.get("helperGenerator"); // transform-runtime 在此处拦截 实现了自己的generator if (generator) { const res = generator(name); if (res) return res; }
- 这一段代码,首先加载了 - helper, 创建了对应的 ast nodes, 后面主要是处理- helper依赖其他- helper的情况,如果存在,就调用- addHelper动态添加。并且如果你的代码里有和- helper一样的方法名,也会进行重命名。js- helpers().ensure(name, File); const uid = (this.declarations[name] = this.scope.generateUidIdentifier(name)); const dependencies = {}; for (const dep of helpers().getDependencies(name)) { dependencies[dep] = this.addHelper(dep); } const { nodes, globals } = helpers().get( name, (dep) => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()) ); globals.forEach((name) => { if (this.path.scope.hasBinding(name, true)) { this.path.scope.rename(name); } }); nodes.forEach((node) => { node._compact = true; });- helpers().ensure(name, File); const uid = (this.declarations[name] = this.scope.generateUidIdentifier(name)); const dependencies = {}; for (const dep of helpers().getDependencies(name)) { dependencies[dep] = this.addHelper(dep); } const { nodes, globals } = helpers().get( name, (dep) => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()) ); globals.forEach((name) => { if (this.path.scope.hasBinding(name, true)) { this.path.scope.rename(name); } }); nodes.forEach((node) => { node._compact = true; });
- 最后这一段, 其实就是把 - nodes, 插入到你的代码中~js- this.path.unshiftContainer("body", nodes); this.path.get("body").forEach((path) => { if (nodes.indexOf(path.node) === -1) return; if (path.isVariableDeclaration()) this.scope.registerDeclaration(path); });- this.path.unshiftContainer("body", nodes); this.path.get("body").forEach((path) => { if (nodes.indexOf(path.node) === -1) return; if (path.isVariableDeclaration()) this.scope.registerDeclaration(path); });
@babel/runtime 的注入逻辑 
在上面我们提到了,helperGenerator方法, transform-runtime就是基于此实现的。
js
const generator = this.get("helperGenerator");
// transform-runtime 在此处拦截 实现了自己的generator
if (generator) {
  const res = generator(name);
  if (res) return res;
}const generator = this.get("helperGenerator");
// transform-runtime 在此处拦截 实现了自己的generator
if (generator) {
  const res = generator(name);
  if (res) return res;
}那这个方法从哪里来的呢?
我们一块定位到如下代码node_modules/@babel/plugin-transform-runtime/lib/index.js
js
pre(file) {
      if (!useRuntimeHelpers) return;
      file.set("helperGenerator", name => {
        if (file.availableHelper && !file.availableHelper(name, runtimeVersion)) {
          return;
        }
        const isInteropHelper = HEADER_HELPERS.indexOf(name) !== -1;
        const blockHoist = isInteropHelper && !(0, _helperModuleImports.isModule)(file.path) ? 4 : undefined;
        const helpersDir = esModules && file.path.node.sourceType === "module" ? "helpers/esm" : "helpers";
        let helperPath = `${modulePath}/${helpersDir}/${name}`;
        if (absoluteRuntime) helperPath = (0, _getRuntimePath.resolveFSPath)(helperPath);
        return addDefaultImport(helperPath, name, blockHoist, true);
      });
      const cache = new Map();
      function addDefaultImport(source, nameHint, blockHoist, isHelper = false) {
        const cacheKey = (0, _helperModuleImports.isModule)(file.path);
        const key = `${source}:${nameHint}:${cacheKey || ""}`;
        let cached = cache.get(key);
        if (cached) {
          cached = _core.types.cloneNode(cached);
        } else {
          // 使用 @babel/helper-module-imports 创建一个导入
          cached = (0, _helperModuleImports.addDefault)(file.path, source, {
            importedInterop: isHelper && supportsCJSDefault ? "compiled" : "uncompiled",
            nameHint,
            blockHoist
          });
          cache.set(key, cached);
        }
        return cached;
      }
    }
  };pre(file) {
      if (!useRuntimeHelpers) return;
      file.set("helperGenerator", name => {
        if (file.availableHelper && !file.availableHelper(name, runtimeVersion)) {
          return;
        }
        const isInteropHelper = HEADER_HELPERS.indexOf(name) !== -1;
        const blockHoist = isInteropHelper && !(0, _helperModuleImports.isModule)(file.path) ? 4 : undefined;
        const helpersDir = esModules && file.path.node.sourceType === "module" ? "helpers/esm" : "helpers";
        let helperPath = `${modulePath}/${helpersDir}/${name}`;
        if (absoluteRuntime) helperPath = (0, _getRuntimePath.resolveFSPath)(helperPath);
        return addDefaultImport(helperPath, name, blockHoist, true);
      });
      const cache = new Map();
      function addDefaultImport(source, nameHint, blockHoist, isHelper = false) {
        const cacheKey = (0, _helperModuleImports.isModule)(file.path);
        const key = `${source}:${nameHint}:${cacheKey || ""}`;
        let cached = cache.get(key);
        if (cached) {
          cached = _core.types.cloneNode(cached);
        } else {
          // 使用 @babel/helper-module-imports 创建一个导入
          cached = (0, _helperModuleImports.addDefault)(file.path, source, {
            importedInterop: isHelper && supportsCJSDefault ? "compiled" : "uncompiled",
            nameHint,
            blockHoist
          });
          cache.set(key, cached);
        }
        return cached;
      }
    }
  };其中关键点在这段代码
它为我们拼接了最终 helper 对应的路径,例如@babel/runtime/helpers/asyncToGenerator
最后再调用addDefaultImport, 添加 import 代码,在我们的代码里
好的以上就是helper注入的逻辑,后面我将分析@babel/preset-env的内容,大家敬请期待。
五、总结 
在一开始,我们一块了解了@babel/helper的具体作用,以及一些缺点。
然后我们又了解了@babel/runtime到底是干啥的,其实就是模块化导入的helper。
最后我们了解了helper的注入逻辑。
当然,最后还有一点悬念@babel/plugin-transform-runtime真正的原理是什么。后面文章将逐步分析。