0%

core-js及垫片理念

core-js是一个javascript标准库,它包含了ECMAScript 2020 在内的多项特性的polyfills,以及ECMAScript在proposals阶段的特性、WHATWG/W3C新特性等。因此它是一个现代化前端项目的 “标准套件”

除了core-js本身的重要性,它的实现理念、设计方式都值得我们学习。事实上,core-js是一扇大门:

  • 通过core-js,我们可以窥见前端工程化的方方面面
  • core-js又和babel深度绑定,因此学习core-js,也能帮助开发者更好地理解babel生态,进而加深对前端生态的理解
  • 通过对core-js的解析,我们正好可以梳理前端一个极具特色的概念-polyfill

core-js工程一览

core-js是一个由lerna搭建的Monorepo风格的项目,在它的packages中,我们可以看相关的五个包:

  • core-js
  • core-js-pure
  • core-js-compact
  • core-js-builder
  • core-js-bundle

core-js 实现的基础垫片能力,是整个 core-js 的逻辑核心

比如我们可以按照如下代码引入全局polyfills:

import 'core-js';

或者按照:

import 'core-js/features/array/from';

的方式,按需在业务项目入口引入某些polyfills。

如何复用一个Polyfill实现

Array.prototype.every 是一个常见且常用的数组原型上的方法。该方法用于测试一个数组内所有元素是否都能通过某个指定函数的测试,并最终返回一个布尔值来表示测试是否通过

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
29
30
31
if (!Array.prototype.every) {
Array.prototype.every = function (callbackfn, thisArg) {
'use strict'
var T, K;
if (this == null) {
throw new TypeError('this is null or not defined')
}
var O = Object(this)
var len = O.length >>> 0

if (typeof callbackfn !== 'function') {
throw new TypeError();
}
if (arguments.length > 1) {
T = thisArg
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
var testResult = callbackfn.call(T, kValue, k, O);
if (!testResult) {
return false
}
}
k++;
}
return true;
}
}

核心思路很好理解:我们通过遍历数组,对数组的每一项调用CALLBACK求值进行返回是否通过测试。但是站在工程化的角度,从core-js这样一个大型项目的视角出发,就不是那么简单了。比如,我们知道core-js-pure不同于core-js,它提供了不污染明明空间的引用方式,因此Array.prototype.every的polyfill核心逻辑实现,就需要被core-js-pure和core-js同时引用,只要分最后导出的方式即可,那么按照这个思路,我们如何实现最大限度的复用呢?

实际上,Array.prototype.every 的 polyfill 核心逻辑实现在./packages/core-js/modules/es.array.every.js中,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';
var $ = require('../internals/export');
var $every = require('../internals/array-iteration').every;
var arrayMethodIsStrict = require('../internals/array-method-is-strict');
var arrayMethodUsesToLength = require('../internals/array-method-uses-to-length');
var STRICT_METHOD = arrayMethodIsStrict('every');
var USES_TO_LENGTH = arrayMethodUsesToLength('every');
$({ target: 'Array', proto: true, forced: !STRICT_METHOD || !USES_TO_LENGTH }, {
every: function every(callbackfn /* , thisArg */) {
// 调用 $every 方法
return $every(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined);
}
});

对应$every源码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var bind = require('../internals/function-bind-context');
var IndexedObject = require('../internals/indexed-object');
var toObject = require('../internals/to-object');
var toLength = require('../internals/to-length');
var arraySpeciesCreate = require('../internals/array-species-create');
var push = [].push;
// 对 `Array.prototype.{ forEach, map, filter, some, every, find, findIndex }` 等方法进行接模拟和接入
var createMethod = function (TYPE) {
// 通过魔法数字来表示具体需要对哪种方法进行模拟
var IS_MAP = TYPE == 1;
var IS_FILTER = TYPE == 2;
var IS_SOME = TYPE == 3;
var IS_EVERY = TYPE == 4;
var IS_FIND_INDEX = TYPE == 6;
var NO_HOLES = TYPE == 5 || IS_FIND_INDEX;
return function ($this, callbackfn, that, specificCreate) {
var O = toObject($this);
var self = IndexedObject(O);
// 通过 bind 方法创建一个 boundFunction,保留 this 指向
var boundFunction = bind(callbackfn, that, 3);
var length = toLength(self.length);
var index = 0;
var create = specificCreate || arraySpeciesCreate;
var target = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined;
var value, result;
// 遍历循环并执行回调方法
for (;length > index; index++) if (NO_HOLES || index in self) {
value = self[index];
result = boundFunction(value, index, O);
if (TYPE) {
if (IS_MAP) target[index] = result; // map
else if (result) switch (TYPE) {
case 3: return true; // some
case 5: return value; // find
case 6: return index; // findIndex
case 2: push.call(target, value); // filter
} else if (IS_EVERY) return false; // every
}
}
return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target;
};
};
module.exports = {
forEach: createMethod(0),
map: createMethod(1),
filter: createMethod(2),
some: createMethod(3),
every: createMethod(4),
find: createMethod(5),
findIndex: createMethod(6)
};

同样是使用了遍历的方式,并由../internals/function-bind-context提供 this 绑定能力,用魔法常量处理forEach、map、filter、some、every、find、findIndex这些数组原型方法的不同方法。

重点来了,在 core-js 中,作者通过../internals/export方法导出实现原型,源码如下:

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
29
module.exports = function (options, source) {
var TARGET = options.target;
var GLOBAL = options.global;
var STATIC = options.stat;
var FORCED, target, key, targetProperty, sourceProperty, descriptor;
if (GLOBAL) {
target = global;
} else if (STATIC) {
target = global[TARGET] || setGlobal(TARGET, {});
} else {
target = (global[TARGET] || {}).prototype;
}
if (target) for (key in source) {
sourceProperty = source[key];
if (options.noTargetGet) {
descriptor = getOwnPropertyDescriptor(target, key);
targetProperty = descriptor && descriptor.value;
} else targetProperty = target[key];
FORCED = isForced(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced);
if (!FORCED && targetProperty !== undefined) {
if (typeof sourceProperty === typeof targetProperty) continue;
copyConstructorProperties(sourceProperty, targetProperty);
}
if (options.sham || (targetProperty && targetProperty.sham)) {
createNonEnumerableProperty(sourceProperty, 'sham', true);
}
redefine(target, key, sourceProperty, options);
}
};

对应我们的 Array.prototype.every源码,参数为:target: ‘Array’, proto: true,表明 coe-js 需要在数组 Array 的原型上,以“污染数组原型”的方式来扩展方法。

而 core-js-pure 则单独维护了一份 export 镜像../internals/export
同时,core-js-pure 包中的 Override 文件,实际上是在构建阶段,复制了 packages/core-js/ 内的核心逻辑,同时提供了复写这些核心 polyfills 逻辑的能力,也是通过构建流程,进行 core-js-pure/override 替换覆盖:

1
2
3
4
5
6
7
{
expand: true,
cwd: './packages/core-js-pure/override/',
src: '**',
dest: './packages/core-js-pure',
}

这是一种非常巧妙的“利用构建能力,实现复用”的方案。但我认为,既然是 Monorepo 风格的仓库,也许一种更好的设计是将core-js 核心 polyfills 再单独拆到一个包中,core-js 和 core-js-pure 分别进行引用——这种方式更能利用 Monorepo 能力,且减少了构建过程中的魔法处理。

寻找最佳polyfill方案

简单来说,polyfill就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该特性
随着前端的发展,尤其是 ECMAScript 的迅速成长以及浏览器的频繁更新换代,前端使用 polyfills 技术的情况屡见不鲜。那么如何能在工程中,寻找并设计一个“最完美”的 polyfill 方案呢?注意,这里的完美指的是侵入性最小,工程化、自动化程度最高,业务影响最低。

第一种方案:手动打补丁。这种方式最为简单直接,也能天然做到“按需打补丁”,但是这不是一种工程化的解决方案,方案原始难以维护,同时对于polyfill的实现要求较高
于是,babel-polyfill 结合 @babel/preset-env + useBuiltins(entry) + preset-env targets 的方案如今更为流行,@babel/preset-env 定义了 Babel 所需插件预设,同时由 Babel 根据 preset-env targets 配置的支持环境,自动按需加载 polyfills,使用方式如下:

1
2
3
4
5
6
7
8
{
"presets": [
["@babel/env", {
useBuiltIns: 'entry',
targets: { chrome: 44 }
}]
]
}

这样我们在工程代码入口处的:

import '@babel/polyfill';

会被编译为:

1
2
import "core-js/XXXX/XXXX";
import "core-js/XXXX/XXXXX";

这样的方式省力省心。也是 core-js 和 Babel 深度绑定并结合的典型案例。

总结

从前端项目的影响来讲,core-js不只是一个polyfill仓库;从前端技术设计的角度来看,core-js也能让我们获得更多启发和灵感。