为Babel做贡献:记住的三个教训
弄清楚一个新的代码库总是充满挑战,Babel 也不例外。
🌐 Getting to work your way around a new code base always poses its challenges, and Babel was no exception.
我一直在作为谷歌2017年夏季编程项目的一部分与Babel合作,致力于更新Babel转换器和Babylon解析器以适应规范的变化,并实现新功能。
🌐 I’ve been working with Babel as part of the Google Summer of Code 2017 program, working to update Babel transforms and the Babylon parser to accommodate changes to specifications and implementing new features.
这是我到目前为止从冒险中学到的一些东西。
🌐 Here’s a few things I’ve learnt from my adventures so far.
1. 是的,沟通很重要
🌐 1. Yes, communication is important
为了更好地了解代码库,我浏览了 Babel 的开放问题列表,找到了一个相对容易处理的([问题 #5728][#5728])问题。
🌐 To start off with getting to know the codebase better, I combed through the open issues list on Babel and found a relatively easy one ([issue #5728][#5728]) to deal with.
只是为了确认我自己知道自己在做什么,我在帖子上快速抛了个问题:
🌐 Just to make sure I knew what I was doing, I fired a quick question on the thread:
在弄清楚情况后,我就开始去修改插件,让它在转译时不抛出“运行时”错误,而只有在代码真正运行时才抛出。有一段明显有问题的代码特别引人注目:
🌐 After getting clarification, I set off to change the plugin to not throw "runtime" errors during transpilation, but only when the code is actually being run. One incriminating piece of code stuck out:
for (const violation of (binding.constantViolations: Array)) {
throw violation.buildCodeFrameError(messages.get("readOnly", name));
}
现在这里需要做的实际上是将一个 throw 语句插入到生成的代码中,这并不太难。不过,仍然还有一些情况会在其他地方抛出运行时错误,而这些代码并不直接与这个文件相关。
🌐 Now what needed to be done here was to actually insert a throw statement into the generated code, which didn’t prove to be too difficult. However, there were still a few cases where runtime errors were being thrown elsewhere from code that wasn’t directly related to this file.
想去探索 Babel 代码库的其他部分,我先把它记下来,以后再继续处理。
🌐 Wanting to go and explore other parts of the Babel code base, I put that down for me to get on with later.
不久之后,我收到了一个,嗯,有趣的关于这个问题的更新……等等,什么?
🌐 Not too long after, I received a, well, interesting update on the issue… Wait what?
我其实从没说过我在修这个问题,但我以为发帖就意味着我会去处理它。
🌐 I never actually said I was working on fixing the issue, but assumed that posting would have implied I was going to work on it.
哎呀。
🌐 Oops.
[#5728]:https://github.com/babel/babel/issues/5728
2. 快照测试的局限
🌐 2. Where snapshot testing falls short
在出发去另一次狩猎时,我偶然遇到了 [问题 #5656][#5656]:
🌐 After setting off for another hunt, I stumbled across [issue #5656][#5656]:
Arguments deoptimized when shadowed in nested function
This is a feature request (I think). Arguments are not optimized if an inner function shadows the name with a parameter (or rest parameters in my case).
Input code
JavaScriptconst log = (...args) => console.log(...args);function test_opt(...args) {log(...args);}function test_deopt(...args) {const fn = (...args) => log(...args);fn(...args);}...
Expected vs. Current Behavior
I’d expect the code to be optimizable to use .apply( thisArg, arguments ) throughout. However, in test_deopt the outer ...args gets copied just to be passed into the inner fn. I can verify that the problem disappears if I rename either the ...args of test_deopt or of the fn arrow function.
[#5656]:https://github.com/babel/babel/issues/5656
这里发生了什么?
🌐 What’s going on here?
现在发生的情况是,这段代码会生成以下内容:
🌐 Now what was happening was that this code would generate the following:
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 的例子一样。
🌐 See that for section there? Usually this is needed as the arguments object isn’t a real array — for example, if you tried to run arguments.slice(), it would fail miserably.
However, in this case it’s only being passed to Function.prototype.apply. Surprisingly enough, Babel already bothers to optimize this specific case, like in the test_opt example above.
正在尝试修复
🌐 Trying to fix it
那我做了什么呢?我把有问题的文件作为一个新的测试用例,试着看看能不能让输出反映出我想要的结果。
🌐 So what did I do? Adding the problem file as a new test case, I tried to see if I could get the output to reflect what I wanted.
“测试为什么会失败?肯定只要稍微改一下就能自己解决。”
尽管疯狂点击 make test-only 并修改了代码中引用标识符的变换,任何改动结果只是导致另一堆测试失败而已。
🌐 Despite spamming make test-only and modifying the transforms of referenced identifiers within the code, any change just resulted in a different bunch of tests failing instead.
Chromium 调试器很“有趣”
🌐 The Chromium debugger is “fun”
感到痛苦、恼火又困惑,我费了些劲才启动 Node.js 调试器去逐步查看发生了什么。
🌐 Miserable, annoyed and confused, I bothered to fire up the Node.js inspector to step through what was going on.
喝完饮料回来坐到电脑前时,我很高兴地看到硬盘灯疯狂闪烁,电脑几乎卡住了。
🌐 After returning to my computer from a drink break, I’m gladly greeted to my hard disk light thrashing around and a practically hung computer.
通过谨慎地使用 Alt + SysRq + F来稳住我的电脑,我设法处理了事情的流程¹,并弄明白了代码究竟是如何工作的。
即便经历了这一切,我仍然看不出为什么它要删除这段我原本以为是“必要”的代码,而这段代码正是我最初修复时被删除的。
🌐 Even through all that, I still couldn’t see any reason why it was deciding to remove this “necessary” (so I thought) code that was being removed with my original fix.
真正的问题?
🌐 The actual problem?
看到上面显示的错误了吗?整段绿色的代码本不应该出现,尽管它是“预期的”。
🌐 See the error shown above? That entire code in green wasn’t meant to be there, even though it was “expected”.
基本上:测试坏了。太棒了. :/
🌐 Basically: the test was broken. Great. :/
[实际的修复][pr] 涉及创建一个 referencesRest 函数,以确保扩展运算符实际上被应用到原始参数上,而不是被另一个作用域中的变量遮蔽了。
¹:结果发现,把一个大文件夹添加到 DevTools 工作区会导致内存泄漏,最终引起 OOM([我为这个提交了个 bug][bug])。
那我们为什么还用快照测试呢?
🌐 So why do we use snapshot testing then?!
嗯,首先,当你只需要让 Babel 运行你的测试用例来生成预期文件时,创建测试要容易得多。这为我们提供了一个低时间成本的选择,同时还能防止相当大一部分潜在错误。
🌐 Well first off, it's far easier to create tests when all you need to do is ask Babel to run your test case to generate your expected file. This presents to us a low time cost option while protecting against a significant proportion of potential errors.
而且,尤其是像 Babel 这样的程序,用其他方式测试会更难。比如,我们可以检查 AST 的特定节点,但这写起来要花更长时间,而且当你的代码尝试改变转换方式时,也容易出现不明显的问题。
🌐 Also, especially with the type of program Babel is, it would be far harder to test for in other ways. For example, we could check for specific nodes of the AST, but this takes far longer to write and is also prone to non-obvious breakage when your code attempts to change the way the transform is done.
所以,总的来说,这里有几个教训:
🌐 So, all in all, a few lessons here:
- 确保你的测试一开始就做对——别自满!
- 是的,调试器实际上在观察发生了什么时很有用。
- 有时候事情需要时间才能顺利解决——如果你没有进展,就剩余一下或者做点别的事。
3. 团队会议!
🌐 3. Team meetings!
我知道这有点牵强地算作一个“问题”,不过无所谓 :)
🌐 I know this kinda stretches the notion of an “issue”, but anyway :)
当你和一群人一起做项目时,互相交流并讨论需要改进的方面总是很有用的。
🌐 When you’re working on a project with a bunch of other people, it’s always useful to catch up with one another and discuss areas which we need to work on.
那我们到底该怎么做啊?!
🌐 So how exactly do we go about doing that?!
啊,开会。
🌐 Ugh, meetings.
当你有一群人分布在世界各地时,找到沟通的方式从来都不容易,但无论如何,我们都得尽力尝试这一壮举。
🌐 When you have a bunch of people spread across the world, finding ways to communicate is never easy, but regardless we would have to make do with our attempts at this feat.
时区
🌐 Time zones
当你处理一个遍布全球的开源项目时,快速选定一个合适的时间很快就会变成一项相当复杂的‘无关紧要的争论’练习。
🌐 When you’re dealing with a open source project spanning all across the globe, picking an appropriate hour quickly turns a rather involved exercise in bikeshedding.
即使我们之间相隔甚远,似乎我们还是勉强能凑在一起做点什么。
🌐 Even with the vast spread between each of us, it seemed like we could just about manage to finally get something together.
唉,这种情况没能持续多久。最终,我们不得不每隔一周在两个时间之间切换以照顾其他用户(UTC 13:00 和 16:00),这意味着我每两周才能参加一次。
🌐 Alas, this was not to last. Eventually, we ended up having to switch between two times every other week to accommodate other users (13:00 and 16:00 UTC), which meant that I was only able to attend once a fortnight.
尽管如此,我们还是设法在协调对 Babel 各个部分进行修复方面取得了重大进展,这些部分涉及关键的更改,包括对 TypeScript 的支持、转换插件运行顺序的更改,以及跟上 TC39 的更新。
🌐 Despite this, we’ve managed to make significant progress with coordinating fixes to various parts that make up key changes to Babel, including support for TypeScript, changes to the order in which transform plugins run, as well as keeping up to date with changes from TC39.
接下来去哪儿?
🌐 Where to next?
我们正在继续打磨 Babel 7,使其适合大众使用,同时也会加入一些新功能。
🌐 We’re continuing to polish up Babel 7 for general consumption, with a number of new features coming along with that.
我正在和一群人一起努力,让更新的 Class Fields 规范提案被包含到 Babel 中,这样大家就可以测试并提供反馈。
🌐 I’m working with a bunch of others to get support for updated Class Fields specification proposal included into Babel so that people can test it out and provide feedback.
顺便说一下,我还想感谢所有 Babel 的导师和贡献者,感谢你们在同行评审上提供帮助,并从第一次接触到现在一直指导我的提案。
🌐 Also, while I’m at it, I’d like to thank all of the Babel mentors and contributors for helping me out with peer reviews and providing guidance with proposals, all the way from first contact to today.
想了解更多关于 Babel 的信息吗?快来看看我们的 贡献页面 并加入 Slack 社区 吧!
🌐 Looking to find out more about Babel? Hit up our contributing page and join the Slack community!
更多关于卡尔的信息
🌐 More about Karl
Karl Cheng 是一位来自澳大利亚悉尼的 GSoC 2017 学生。在 GitHub (Qantas94Heavy) 和 Twitter (@Qantas94Heavy) 上了解更多关于他的信息!
🌐 Karl Cheng is a GSoC 2017 student hailing from Sydney, Australia. Find out more about him on GitHub (Qantas94Heavy) and Twitter (@Qantas94Heavy)!
请查看我们关于 代码之夏 的第一篇文章,了解更多信息!
🌐 Please check out our first post on Summer of Code for more info!