跳至主内容

参与 Babel 贡献:三个值得牢记的经验教训

· 1 分钟阅读
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

熟悉一个新的代码库总会带来挑战,Babel 也不例外。

作为 Google Summer of Code 2017 项目的一部分,我一直在参与 Babel 的工作,致力于更新 Babel 的转换器和 Babylon 解析器,以适应规范的变化并实现新功能。

以下是我到目前为止从这段经历中学到的几点心得。

1. 没错,沟通很重要

为了更深入地了解代码库,我仔细翻阅了 Babel 的开放问题列表,并找到了一个相对容易处理的问题(issue #5728)。

为了确认我的理解无误,我在该讨论串中快速提了一个问题:

My question asking for clarification

得到澄清后,我开始着手修改插件,使其在转译期间不抛出“运行时”错误,而只在代码实际运行时抛出。一段引人注目的代码引起了我的注意:

JavaScript
for (const violation of (binding.constantViolations: Array)) {
throw violation.buildCodeFrameError(messages.get("readOnly", name));
}

现在需要做的是在生成的代码中实际插入一个 throw 语句,这并不算太难。然而,仍然存在一些情况,运行时错误会在其他地方抛出,而这些错误与当前文件并无直接关系。

由于我想去探索 Babel 代码库的其他部分,便将这个问题暂时搁置,留待后续处理。

没过多久,我就收到了一个关于该问题的、呃、有趣的更新……等等,什么情况?

Someone else had claimed the issue.

我其实从未明确表示我正在着手修复该问题,但以为发帖就暗示了我打算处理它。

哎呀。

2. 快照测试的不足之处

在开始另一次问题搜寻时,我偶然发现了 issue #5656

当在嵌套函数中被遮蔽时,参数被取消优化

这是一个功能请求(我认为)。如果内部函数通过参数(或在我的情况下是 rest 参数)遮蔽了该名称,则参数不会被优化。

输入代码

JavaScript
const log = (...args) => console.log(...args);

function test_opt(...args) {
log(...args);
}

function test_deopt(...args) {
const fn = (...args) => log(...args);
fn(...args);
}

...

预期行为与当前行为

我期望代码能够被优化,从而在整个过程中使用 .apply( thisArg, arguments )。 然而,在 test_deopt 中,外层的 ...args 被复制,只是为了传递给内部的 fn。 我可以验证,如果重命名 test_deopt 的 ...args 或 fn 箭头函数的 ...args,问题就会消失。

这是怎么回事?

现在的情况是,这段代码会生成如下内容:

JavaScript
var log = function log() {
var _console;

return (_console = console).log.apply(_console, arguments);
};

function test_opt() {
log.apply(undefined, arguments);
}

function test_deopt() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { // unnecessary loop
args[_key] = arguments[_key];
}

var fn = function fn() {
return log.apply(undefined, arguments);
};
fn.apply(undefined, args);
}

看到那里的 for 循环了吗?通常这是必需的,因为 arguments 对象并非真正的数组——例如,如果你尝试运行 arguments.slice(),它会失败得很惨。 然而,在这个例子中,它只是被传递给 Function.prototype.apply。令人惊讶的是,Babel 已经费心优化了这种特定情况,就像上面 test_opt 示例中那样。

尝试修复

那么我做了什么呢?我将问题文件添加为一个新的测试用例,试图让输出符合我的期望。

Test failure of modified code

“测试怎么又失败了?稍微改改应该就能解决吧。”

尽管反复执行 make test-only 并修改代码中的标识符转换逻辑,每次改动都只是导致另一批测试失败。

Chromium 调试器的"乐趣"

沮丧又困惑的我终于启动 Node.js 调试器,想一步步看看到底发生了什么。

Using the Chromium debugger

喝完水回到电脑前,迎接我的是疯狂闪烁的硬盘指示灯和几乎卡死的系统。

Chromium process using more than 3GB of memory

Holding my computer together with judicious applications of Alt + SysRq + F, I managed to work through the flow of things¹ and figure out how exactly the code worked.

即便如此,我还是没明白为什么调试器认定这段"必要"代码(我最初认为)应该被移除——而这正是我原始修复方案删除的部分。

真正的问题?

看到上面标绿的代码了吗?这段代码本不该存在,尽管测试"预期"它出现。

简而言之:测试用例本身有缺陷。真是棒极了。 :/

真正的修复方案 是创建 referencesRest 函数,确保展开运算符作用于原始参数,而不是被其他作用域的变量遮蔽。

注¹:后来发现向 DevTools 工作区添加大型文件夹会导致内存泄漏直至 OOM(我提交的相关 bug 报告)。

那我们为什么还要用快照测试?!

首先,创建测试极其便捷——只需让 Babel 运行测试用例生成预期文件即可。这种方式时间成本低,同时能防范大部分潜在错误。

特别是对于 Babel 这类程序,其他测试方式会更复杂。例如检查 AST 特定节点不仅编写耗时,当转换逻辑变更时也容易隐式失效。

综上,我总结了几个经验:

  1. 确保测试用例本身正确——永远不要想当然!

  2. 调试器确实能帮你看清执行过程

  3. 有些问题需要时间沉淀——毫无头绪时不妨暂停或切换任务

3. 团队会议!

虽然这不算严格意义的"issue",但还是值得说说 :)

在多人协作项目中,定期交流讨论工作重点总是有益的。

那么具体怎么操作呢?!

唉,又是开会。

当成员遍布全球时,协调沟通本就不易,但我们仍尽力找到了解决方案。

时区难题

在跨时区的开源项目中,选择合适会议时间很快变成了繁琐的协调工作。

World map of people who’ve attended our meetings

尽管时差跨度很大,我们最终还是勉强敲定了方案。

Time zones discussed in 31 May 2017 meeting

可惜好景不长。为照顾其他成员,我们不得不采取双周轮换制(UTC 13:00 和 16:00),这意味着我每两周才能参加一次会议。

尽管如此,我们在协调 Babel 关键变更的各个部分修复方面取得了显著进展,包括对 TypeScript 的支持、转换插件执行顺序的调整,以及紧跟 TC39 的最新规范变更。

后续计划

我们正在持续完善 Babel 7 以供广泛使用,届时将推出多项新功能

我正与其他贡献者合作,将更新后的类字段(Class Fields)规范提案支持集成到 Babel 中,方便开发者测试并提供反馈。

借此机会,我要衷心感谢所有 Babel 导师和贡献者,从初次接触到今天,他们始终在代码评审和提案指导方面给予我极大帮助。


想了解更多关于 Babel 的信息?请访问我们的贡献指南页面并加入 Slack 社区

关于 Karl 的更多信息

Karl Cheng 是来自澳大利亚悉尼的GSoC 2017学生。欢迎在 GitHub (Qantas94Heavy) 和 Twitter (@Qantas94Heavy) 关注他!

更多详情请参阅我们关于Summer of Code的首篇博文!