ES6中Generator函数的设计问题以及co.js的原理
** 本文适合有一定JS经验的开发者阅读 **
从很久以前开始我对于web前端的开发就不报以好感,js可以算是web前端的技术代表。 js自问世以来就伴随着许多设计问题,比如错误的闭包设计,诡异的prototype。 许多的开发者在此付出了很多(没有必要的)精力才把js逐渐推进现代化。
然而ES6还仅仅只能算现代化的开端,其中还有一些在我看来的错误的设计。 其中有一处错误设计就是Generator函数中的yield函数和next函数。 然而这一错误设计却被巧妙利用了起来,并催生了co.js这种大热的函数库。
Generator的设计错误
Generator函数的设计本意应该是类似于Python的generator,能够使程序出现一种懒惰(Laziness)的感觉。 (Laziness是被Haskell,Clojure等现代函数式编程语言所推广的一项特性,能够在特定条件下改善性能,恕我不展开例子。) 然而在Generator的函数实现里,却出现了 Sending values to the generator 的神奇特性。 在链接里,可以看到,通过调用
g.next(2);
可以使generator的行为改变,具体来看,
就是代码定义中的var value = yield null
会在本质上被替换成yield null; var value = 2
。
我并非PL类的大神,但这在我来看是个错误设计,原因有两点:
- Generator的行为变成了非决定性(non-deterministic)的, 虽然js本来也并非函数式语言,然而我认为非决定性对于阅读代码,调试代码都相当不利。
- 隐式的传参,破坏了程序的语意结构。同样,这一点会造成对源码阅读的困难。 然而却正是语意的破坏,才使这个设计缺陷被co.js利用起来。
错误设计的巧用和co.js的原理
我不喜欢写长篇大论。co.js的所实现的东西用一句话来概括就是: 能用同步的语法来写异步语句:
此功能类似于ES7的await
,能够有效降低异步程序书写的复杂度。
这功能看起来很厉害,实际很简单。
如何实现呢?其实很简单,我写了个程序实现了一个精简版的co.js,姑且叫myco
。
这个程序去除了容错以及对于this
的处理,其余的原理应该和co.js是一致的。
其核心机制是在g.next()
的时候,把promise的resolve结果传入作为参数。
利用上面提及的“破坏程序语意”的设计,让异步操作在代码上可以看起来有同步的效果。
为啥?
看到以上,可能你会有几个问题:
- 我们做了什么?:其实很简单,利用一个设计错误,做出了一个精简版的co.js库。
- 这有什么意义?:实现了一个
await
的子集,可以大大降低异步代码的bug率。 - 我们做了创新?:并没有,同样的概念在Clojure里有成熟,强大的实现(
promise
,future
)。
为什么在PL语言的发展中会出现这种诡异的现象呢? 语言设计有问题落后于时代,社区却拼命把这个语言实现现代化,甚至把缺陷也加以利用实现新功能。 难道是爱?
我想并不是,只是在浏览器大战终结之前,Javascript的地位难以动摇。 强大的市场需求可以对一个残破的技术造成巨大推力,可以看见将来js会有现代化的一天。
然而从这个小问题出发,有一些事实开发者是应该清楚的:JS社区虽然活跃庞大积极,其并不先进。 开发者一方面在JS的区间里要勇于尝试新框架,因为不可否认这些新鲜框架是JS的未来的stepstone。 另一方面在JS之外应该积极尝试新语言,因为毕竟开发者除了开发,还应该有点hack的精神,不能止步不前固步自封。