最近看了些项目的源码,其中就包括 thunkify
。虽然 thunkify
代码简单,很快就读完了,不过看看项目的测试文件和提交历史,也还是能看出很多事物的。这次阅读的代码是当前最新版本,commit 编号 0bd83e。
功能陈述
将一个函数转换为一个为 Thunk 函数,这个函数被调用后会返回一个以回调函数为参数的函数。可以参考 Thunk 函数。
简单实现
因为需求比较简单,所以我自己先实现了一下,然后比较与官方仓库的差异,可以发现一些源码的特点。呢
我自己的实现:
1 | module.exports = function (fn) { |
从测试看问题
源码有 test 文件,可以将源码的测试文件 clone 到本地后测试。将上面的代码测试后可以发现 3 个测试未通过:
- thunkify(fn) should maintain the receiver
- thunkify(fn) should catch errors
- thunkify(fn) should ignore multiple callbacks
maintain the receiver
通过 test 文件代码发现,这里主要涉及到一个关于 this
的问题:
1 | function load (fn) { |
原函数 f 可能是某个对象的方法,所以要保证 thunkify 后的函数,称它为 tf, 仍然能正常访问 this
,所以 tf 函数里需要能引用到原函数 f 里 this
的值,thunkify
源码中用 ctx
变量对其表示原函数 f 的 this
:
1 | var ctx = this; // line 27 |
catch errors
尽管原函数 f 是需要一个回调函数做参数,理论上这个回调函数应该能捕捉异常了,但有些时候,这个回调函数可能没有正常 catch,例如测试文件中的样例:
1 | // fn 作为回调函数却没有捕捉这个异常 |
而 thunkify 则 “帮” 其捕捉了。当发现原函数 f 抛出异常时,源码中自动用用户传进的回调函数捕捉了。
1 | // line 42 - 47 |
ignore multiple callbacks
理论上讲,一个回调函数 callback 只能被调用一次,但在实际情况中,仍然会存在被调用多次的意外情况,例如测试文件中的例子:
1 | function load (fn) { |
为了确保回调函数 callback 只被调用了一次,thunkify
对回调函数进行了一次封装:
1 | // line 34 - 40 |
called
作为一个 flag,第一次调用时,if 语句会把 called 当作否定值,所以 return
不会被执行。但从第二次开始,called
都会变成 true
,所以 return
都会执行,确保了回调函数 callback 只会被调用一次。
这里需要注意一下 args
这个变量,它每次都会 push
一下,因为 thunkify
后的函数 tf 可能被引用调用多次:
1 | function fn (done) { |
这个程序最后只会输出一次值。因为根据闭包规则,第二次调用 tf 开始,回调函数就被 push
进 args
里了。当第三次调用 tf 时,此时的 args 等价为 [c1, c2, c3]
,根据源码, tf(c3)
相当于调用 tf.apply(ctx, [c1, c2, c3])
。所以此时真正执行的回调是 c1
,如前文所说,这个 c1
是被源码封装过的,里面的内容只会被执行一次。
自己的实现(改进后)
结合 ES6,自己在解决上述问题后又实现了一遍,功能上没有改变,全是增加鲁棒性
1 | module.exports = function (fn) { |
从 Commit 看改进
查看各个 commit,以及以前的代码,可以发现一些有趣的事。
crankshaft
在 d53746 这个 commit 中,提交者改变了 arguments 变成了数组的方式,从简单的 slice 方法,变成了声明一个数组然后一一赋值的方法。
第一次看到 crankshaft
还不知道是什么,后来才知道是指代 Chrome 的一个引擎。Pull request #12 有提到这个优化,虽然我还是觉得这个优化在某种程度牺牲了部分可读性。
remove memoization
参考 30f25a 移除了一个记忆化操作。
在这个版本之前的代码,如果执行下面的程序,会发现这三次执行都输出同样的结果,这多多少少有点反直觉,所以 commit 上 tj 也说 promises have different expectations
。
1 | function fn (done) { |
所以这个改进后,执行上述程序只会输出一次结果。
add assert(fn)
在 05abda 处增加了一个 assert 调用避免被 thunkify 的参数不是函数 – 一个提高函数鲁棒性的功能。
remove support for eager execution
a504b9 算一次比较大的改进,发现这个 commit 大大缩减了代码。不过这次删去了避免回调函数被多次执行的代码,也就是对回调函数的封装。在这之后的几个 commit 里,维护者又把这一层封装添加了回去。
总结
在没有看过 thunkify
源码的情况下,这个功能的实现并不算难。但 thunkify
代码比在本文开始我自己写的代码更优秀的地方就在于代码的鲁棒性,这一点从测试文件和 commit 日志中可见一斑,确实考虑了生产环境中可能出现的众多复杂情况,更适合日常使用。同时,阅读 thunkify
源码的目的也并不限于代码的实现,更多的是学习维护和增加代码的鲁棒性。