当前前端的开发,已经基本离不开babel的使用了。

babel下两员大将babel-polyfilltransform-runtime也经常出现在各位前端同胞的业务及库的开发中,让我们得以欢快地使用js的各种高级语法以及牛X的api。可虽说是如此常用,很多前端同胞甚至是前端老司机也常常搞不清二者的关系,以至陷入莫名的疑惑当中。

正在这个时候,一名不愿露面的网友,本着刨根问底的钻(qiu)研(nue)精神,一窥二者的本质,向大家聊一聊两者的关系。

在此之前,让我们先了解一下core-js这个库,版本2.5.7。打开它的github主页,如下:

core-js

我们发现,呃,作者正在找工作,这个不是重点。从这段文字中我们知道了core-js是一个模块化的js标准库,包含着各种垫片等。重点在最后一句,我们可以按需使用,或者不污染全局命名空间地使用它

也就是说我们至少应该有两种方式使用它:

  1. 在程序入口的最开始处引入,当成垫片使用,会污染全局命名空间。

    1
    require('core-js/shim')
  2. 当成一个普通的commonjs库使用,不污染全局命名空间。

    1
    var core = require('core-js/library')
  3. 实际上,还有第三种,一股脑全引入,同样会污染全局命名空间:

    1
    require('core-js')

好的,回到我们的主角当中。

babel-polyfill

我们先来看看babel-polyfill,以版本6.26.3为例,找到安装包的入口文件lib/index.js,简洁的数行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (global._babelPolyfill) {
throw new Error("only one instance of babel-polyfill is allowed");
}
global._babelPolyfill = true;

import "core-js/shim";
import "regenerator-runtime/runtime";

// Should be removed in the next major release:

import "core-js/fn/regexp/escape";

const DEFINE_PROPERTY = "defineProperty";
function define(O, key, value) {
O[key] || Object[DEFINE_PROPERTY](O, key, {
writable: true,
configurable: true,
value: value
});
}

define(String.prototype, "padLeft", "".padStart);
define(String.prototype, "padRight", "".padEnd);

// eslint-disable-next-line max-len
"pop,reverse,shift,keys,values,entries,indexOf,every,some,forEach,map,filter,find,findIndex,includes,join,slice,concat,push,splice,unshift,sort,lastIndexOf,reduce,reduceRight,copyWithin,fill".split(",").forEach(function(key) {
[][key] && define(Array, key, Function.call.bind([][key]));
});

注释后面的就不用看了,将在下一个主版本发布时移除掉。然后我们看到只引入了两个库core-js/shimregenerator-runtime/runtime。前者根据core-js的第一条使用方式,是作为js垫片使用的。后者会提供一个全局函数regeneratorRuntime,这个就厉害了 ,我们之所以能够欢快的使用asyncawait,全是它的功劳。到此我们大概知道了babel-polyfill实际上是一种垫片技术,保证了js在各版本下的一致性。

babel-plugin-transform-runtime

再来看看babel-plugin-transform-runtime,以6.23.0为例,这是一个babel插件,依赖babel-runtime,而babel-runtime又依赖core-jsregenerator-runtime。是不是隐约觉得babel-polyfillbabel-plugin-transform-runtime已经产生了某种联系?

该插件中有一个definitions.js文件,定义了一些builtins(即浏览器内建对象、方法),及一些如ArrayObjectMathStringNumber等类的类方法的映射路径。这些路径如何映射,又映射到哪里呢?

Arrayincludes方法举例说明,definitions.js中这样定义到:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
// builtins: {
// ...
// },
methods: {
Array: {
// ...
includes: "array/includes",
// ...
},
// ...
}
}

babel插件,是对babel转换后的AST(抽象语法树)进行的一系列处理,该插件对我们代码中用到的类方法的处理如下:

1
2
3
4
5
6
7
const moduleName = getRuntimeModuleName(state.opts);
path.replaceWith(
this.addDefaultImport(
`${moduleName}/core-js/${methods[prop.name]}`,
`${obj.name}$${prop.name}`,
),
);

其中moduleName默认是babel-runtime${moduleName}/core-js/${methods[prop.name]}代入变量还原后就是babel-runtime/core-js/array/includes。找到对应路径的文件,代码如下:

1
module.exports = { "default": require("core-js/library/fn/array/includes"), __esModule: true };

引用了core-js/library,根据core-js的第二条使用方式 —— 当成一个普通的commonjs库使用,不污染全局命名空间。而且可以针对某个方法或builtins单独引入。

这里还有个疑问,看过transform-runtime使用文档的会知道,它并不支持实例方法,如[1, 2, 3].includes(1)将无法起作用,那上面的Arrayincludes又是什么情况呢?事实上,它仅仅是提供了一个类方法的使用,及Array.includes,使用方式Array.includes([1, 2, 3], 1),第一个参数接收一个数组实例。

例如,使用transform-runtime

1
2
3
4
5
const a = [1, 3]
// 类方法
console.log(Array.includes(a, 1))
// 实例方法
console.log(a.includes(1))

转换为

1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";

var _includes = require("babel-runtime/core-js/array/includes");

var _includes2 = _interopRequireDefault(_includes);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var a = [1, 3];

console.log((0, _includes2.default)(a, 1));

console.log(a.includes(1));

总结

现在我们总结下两者的区别与应用场景:

  • babel-polyfill 提供完整的补丁,保证各浏览器下的一致性,但体积大,会污染全局变量,适合应用于业务开发当中,开发人员无需过多关注兼容性问题。
  • transform-runtime 按需提供,不支持实例的方法,不污染全局变量,适合于库的开发