专访Wind.js作者老赵(上):缘由、思路及发展

引言

\

Wind.js是很有特点的一个JavaScript异步编程类库(其前身为Jscex),最近作者不但发布了其眼中的里程碑版(v0.6.5),还在“我们的开源项目”系列活动和“阿里技术嘉年华”上连续露脸,获得广泛关注。Wind.js的作者赵劼就在本站担任编辑,InfoQ自然抓住机会对老赵做了正式的书面采访。采访篇幅较长,将分上下集播出。老赵将在上篇中借机倾述他对于库设计的思考和心得。

\

Q:能否先介绍一下你自己?

\

大家好,我是赵劼,在网上一般叫做老赵或者赵姐夫,经常沉迷于代码世界里,关注技术发展或是程序员的成长等话题,而不太愿意接触如今比较“流行”的产品设计,项目管理,产业分析等等,连所谓的“大规模应用架构设计”都不太关心——甚至还略有抵触,所以我可以说是个纯码农。之前呆过外企,呆过民企,创过业,也呆过国内的互联网企业,现在则是在做外资银行的内部系统,可以说是一块没有接触过的领域,痛并快乐着。

\

在工作之余,我也会十分热衷于在业余时间编写自己的开源?作品,其中Wind.js便是迄今为止我最为投入(没有之一)的项目。当然技术之外,我也喜欢烧烧菜,弹弹琴——别看我一副十足的码农范,但我也是从小学琴,标准琴童出身呢。

\

Q:是什么促使你开发Wind.js这个异步编程类库的呢?

\

应该说是需求与兴趣的共同驱使吧。

\

首先,随着Web平台地位的提升,霸占着浏览器的JavaScript语言也成为了世界上最流行的语言之一,甚至通过Node.js进入了服务器编程领域。JavaScript的一个重要特性便是“不能阻塞”,这里的“不能”是指“不应该”而不是“无法”的意思(只要提供阻塞的API)。JavaScript是一门单线程语言,因此一旦有某个API阻塞了当前线程,就相当于阻塞了整个程序,所以“异步”在JavaScript编程中占有很重要的地位。异步编程对程序执行效果的好处这里就不多谈了,但是异步编程对于开发者来说十分麻烦,它会将程序逻辑拆分地支离破碎,语义完全丢失。因此,许多程序员都在打造一些异步编程模型已经相关的API来简化异步编程工作,例如Promise模型。而我开发Wind.js,其实也是出于这个目的,只不过使用的方式有些特别,效果更好。

\

其次,我本身一直是个编程语言爱好者,对这方面的关注经常不被人理解,被鄙视也不是一次两次了。Wind.js主要受到了F#的启发,F#是微软为.NET平台创建的一门函数式编程语言,以OCaml为基础,并加入了一些前沿的编程语言研究成果。其中很重要的一个特性便是“计算表达式”,它对于异步编程的支持令人眼前一亮。Wind.js的前身是Jscex,即JavaScript Computation EXpressions的缩写,它是F#中“计算表达式”特性在JavaScript编程语言上的体现,因此理论上说它不仅仅只有“异步编程”,但简化异步编程的确是它最重要的一部分功能,没有之一。

\

这么看来,我为JavaScript本身引入其他语言的异步编程特性也是顺理成章的事情吧。

\

Q:正如你所说的那样,在简化JavaScript异步编程这个事情上其实已经有很多不同的方案(例如Promise等异步模型),那么你觉得这些现成的方案都有哪些不足之处,Wind.js是否有“重新发明轮子”的意味在里面呢?

\

作为一个异步流程控制类库,Wind.js的唯一目的便是“改善编程体验”,这跟其他大大小小异步流程控制方案有着相同的目标——但是,改善的“程度”以及改善的“方式”便是Wind.js与它们最大的区别。

\

在传统的异步流程控制类库中,人们普遍使用构建各类API的方式来辅助异步逻辑编写。例如著名的Step类库为了解决异步操作顺序执行的问题,便引入了一个Step方法:

\
\Step(\    function readFile() {\        fs.readFile(filename, this);\    },\    function capitalize(err, text) {\        if (err) throw err;\        return text.toUpperCase();\    },\    function writeBack(err, newText) {\        if (err) throw err;\        fs.writeFile(filename, newText, this);\    },\    function complete(err) {\        if (err) throw err;\        console.log(\"complete\");\    }\);\
\

在Step类库里,假如要将几个系统串联,则可以依次将多个函数顺序提供给Step方法,每个函数内部使用自身的this作为回调,或直接返回一个结果。那么这里就有一个问题,例如在一个面向对象开发的系统中,它们对this有着严格的依赖关系,而Step则强制使用this保存回调函数,那又该如何协调两者之间的矛盾?当然,对于一个有经验的JavaScript开发人员来说,解决这点自然不是问题,但始终需要“绕”,即便不影响功能,也会影响编程风格(模式)——由于类库的需要而不得不引入的编程风格(模式)。

\

串行如此,那么并行呢?Step又引入了parallel函数来实现这方面需求,换句话说,每增加一类需求,我们则往往需要增加一个甚至一套API。Step提供了“串行”、“并行”、以及“分组”三类执行方式,而功能更为丰富的Async类库则提供了十几种异步流程控制或是数据处理的辅助方法。

\

这就产生了矛盾:更强大的类库,则往往意味着更多的API,以及更多的学习时间,这样开发者才能根据自身需求选择最合适的方法。那么,多少API才能够满足广大人民群众的需求呢?这其实很难界定,根据80/20法则,80%的用户只需要20%的功能,但其实不同的用户群体需要的那20%可能各不相同,这几乎意味着一个类库为了满足各种多变的需求,则需要提供100%的功能,让不同的用户各取自身所需的20%功能。而事实上,由于人们总是能够不断地挖掘出新的需求,因此我们时不时需要设计并实现新的API。同时,为了保证现有代码的兼容性,旧的API也必须保留下来,于是类库的规模往往会不断扩大。

\

此外,API的粒度也是一个课题。粒度越大的API往往功能越强,可以通过少量的调用完成大量工作,但粒度大往往意味着难以复用。越细粒度的API灵活度往往越高,可以通过有限的API组合出足够的灵活性,但组合是需要付出“表现力”作为成本的。JavaScript在表现力方面有一些硬伤,即所谓“语法噪音”,例如定义函数所用的function关键字和大括号,函数返回值必须使用的return关键字等等,其实都限制JavaScript在某些场景下的表达能力,例如在CoffeeScript中的匿名函数(x) -\u0026gt; x + 1,使用JavaScript就必须写为function (x) { return x + 1; }。我们的确可以在JavaScript中设计出一套足够灵活,可以组合出各种流程的异步API,我也做过这方面的尝试,但最终会发现,这样设计出来的API始终无法清晰地表达出流程的真正意图。

\

总之,对于传统类库来说,在如何界定类库的“边界”,如何确定API的粒度等方面,都需要做出大量的权衡。所谓“权衡”便是通过牺牲一部分的利益来换取更大的收获,但可能在满足80%的需求的时候,剩余20%的用户只能无奈地离开了。

\

Wind.js的确是个“轮子”,但绝对不是“重新发明”的轮子。我不会掩饰对Wind.js的“自夸”:Wind.js在JavaScript异步编程领域绝对是一个创新,可谓前无来者。有朋友就评价说“在看到Wind.js之前,真以为这是不可能实现的”,因为Wind.js事实上是用类库的形式“修补”了JavaScript语言,也正是这个原因,才能让JavaScript异步编程体验获得质的飞跃。

\

Q:你一直强调Wind.js完全就是JavaScript的编程,包括“编程体验”,为什么特别纠结这一点?你为Wind.js选择了比较特别的实现方式,也跟这个思路有关吗?这种实现方式的优势在哪里?

\

我们现在想要解决的是“流程控制”问题,那么说起在JavaScript中进行流程控制,有什么能比JavaScript语言本身更为现成的解决方案呢?在我看来,流程控制并非是一个理应使用类库来解决的问题,它应该是语言的职责。只不过在某些时候,我们无法使用语言来表达逻辑或是控制流程,于是退而求其次地使用“类库”来解决这方面的问题。

\

这方面我可以举一个例子,且看这段简单的代码:

\
\for (var i = -5; i \u0026lt; 5; i++) {\    if (i \u0026lt; 0) {\        console.log(i + \"是负数\");\    } else {\        console.log(i + \"是正整数\");\    }\}\
\

但其实,只要我们提供了合适的_for_if函数(附1),我们完全可以不用forif关键字来实现相同的功能:

\
\_for (-5, 5, function (i) {\    _if (\        // 条件\        i \u0026lt; 0,\        // 满足条件的分支\        function () {\            console.log(i + \"是负数\");\        },\        // else分支\        function () {\            console.log(i + \"是正整数\");\        });\});\
\

其实只要是有一定函数式编程能力(主要是匿名函数)的语言,都可以在一定程度上通过这种形式的类库实现流程控制,只不过因为语言特性不同,各有美丑而已。不得不说,JavaScript虽然有着还算不错的函数式编程特性,但由于之前提到的硬伤,几乎无法优雅地完成这项任务。

\

事实上,上面演示的_for方法的灵活程度与for语句还有很大差距,例如,您不妨尝试下如何使用类库实现如下的for循环:

\
\var iter = getIterator();\for (var i = 0; iter.current \u0026lt; i++; iter.moveNext()) {\    ...\}\
\

这会直接导致我们的_for函数必须接受更多的匿名函数,“享受”更多JavaScript语言的硬伤。

\

我想说的是,即便我们“有能力”用类库实现类似的效果,但真会有人去这么做吗?使用JavaScript语言本身提供的特性进行流程控制,对于JavaScript开发人员来说,是一件天经地义,顺理成章的事情。JavaScript语言已经提供了开发人员控制流程所需的全部关键字,而且开发人员已经无数次证明了这些关键字是多么的灵活与优雅。假如,我们这里先说“假如”——假如我们可以使用传统的JavaScript语言编写异步代码,使用JavaScript来控制流程,表达逻辑,那么我们就可以避免学习大量新的API,避免遭受JavaScript中的语法噪音,我们只需编写简单而熟悉的JavaScript代码即可。

\

在软件开发行业有一句很著名的话:Every abstraction is leaky,换句话说,任何一种抽象都是有缺陷的。事实上,各种异步编程模型都是种抽象,它们是为了实现一些常用的异步编程模式而设计出来的一套有针对性的API。但是,在实际使用过程中我们可能遇到千变万化的问题,一旦遇到模型没有“正面应对”的场景,或是触及这种模型的限制(如Step的this回调),开发人员往往就只能使用一些相对较为丑陋的方式来“回避问题”。Wind.js也是一种抽象,因此也不可避免的出现leaky,但Wind.js的设计思路便是将这种抽象构建为JavaScript本身。换句话说,Wind.js的能力在很大程度上是受限于JavaScript语言的表达能力,而传统异步编程类库则是受限于自身设计的API的表达能力。哪种做法可以应对更大范围的场景,我相信应该已经不言而喻了。

\

所以Wind.js选择的是这么一种方式:开发人员使用最普通不过的JavaScript来表达一段逻辑,只不过通过某种方式来指定某一个环节是“异步操作”,代码执行流程会在这里“暂停”,等待该异步操作结束,然后再继续执行后续代码。如果这个操作失败,则相当于抛出了一个异常,会被catch语句捕获。一段类似的代码可以是这样的:

\
\try {\    var content = $await(read(src, content));\    console.log(\"内容读取成功\");\    $await(write(target, content));\    console.log(\"内容读取成功\");\} catch (ex) {\    $await(submitError(ex));\    console.log(\"错误提交成功\");\}\
\

在这段代码里,我们用$await(\u0026lt;task\u0026gt;)标记一个异步操作,代码流程会在此处“等待”异步操作完成,在catch分支中也一样。我想,任何一个普通的JavaScript程序员都能顺利理解这段代码的含义。至于性能,自然必须和使用传统回调风格写出来的程序完全一致,这里的“等待”并不是“阻塞”,而会空出执行线程,直至操作完成。而且,假如系统本身没有提供阻塞的API,我们甚至没有“阻塞”代码的方法(当然,本就不该阻塞)。

\

当然,单纯依靠JavaScript语言本身显然无法获得这样的能力,JavaScript的执行流程不会随便地暂停,因此我们需要对代码进行自动改写。这便是Wind.js的工作原理,值得注意的是“自动”二字,换句话说开发者编写代码的时候完全不需要额外的“改写”步骤,这个功能在Wind.js中叫做“JIT(Just in Time)编译”,也就是在程序的执行过程中动态生成并执行新的代码。

\

Wind.js自动改写的最小单位是“函数”,传统JavaScript函数是这样定义的:

\
\var func = function () {\    // 函数体\};\
\

而Wind.js函数是这样定义的:

\
\var windFunc = eval(Wind.compile(\"async\
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值