异步编程 Async JavaScript 在 Node 面前获得前所未有的重视。本文结合 Trevor Burnham 所著 《Async JavaScript Build More Responsive Apps with Less Code(中文名: JavaScript 异步编程:设计快速响应的网络应用)》一书,梳理 JavaScript 的异步编程的方方面面。
为更好地了解异步开发的来龙去脉,我们先回顾一下 JS 服务端的历史,看到底解决了什么问题(当然是否真的解决是另外一个问题),并以此来与 Node 横向比较。
实现异步的关键,简单讲,在于“闭包(Closures)”。函数是 JS 为第一等公民,可以把函数作为对象调来调去,并一个函数轻易地包含另外一个函数;或者被另外一个函数包含着,也没有问题。——故所以,在 JS 里面闭包是天然支持的。从泛语言的角度讲,任何有闭包的语言都是函数式语言,区别在于写起来是否轻松,越轻松的话那么越名副其实。这样的话,JS 具备了那么多优点,如果把 JS 应用在非阻塞环境,例如 Socket 网络编程中,是否可行呢? Node 作者 Ryan Dahl 就是这样的想的,于是尝试将 V8 与 非阻塞的 C 代码结合起来,从而诞生了 Node。后来事实证明,Ryan 的方式不仅可行且表现不俗。
综合几个方面,我们不妨再思考下这些问题:
实际上,包括 JAVA 在内的许多 API 如 JDK 都提供 NIO 非阻塞版本之接口。如果使用多线程模型编码,面临着若干问题。先着眼于两点:
多线程模型下固然同样可以处理非阻塞调度,只是相比事件模型消耗的资源来得更大,尤其长连接的场景,最能体现这种不足。另外编码成本也更高。关于两者的分析,小弟在旧文已经探讨过,详见《学习NodeJS第二天:漫谈NodeJS 》。
实际上,不论前端抑或后端,都会遭遇一个问题,就是如何优雅应对复杂事件集的范畴,这仍属于 JavaScript 有待解决的前沿领域。
从1995 年诞生的年份开始起,本属“草根”到不得了的 JavaScript 于 AJAX 革命成功之后“颠覆性”地一路走来并在茁壮地成长,除了 VB Script 正面较量外,其他的 RIA 应用还不算有真正的威胁,Flash、Sliverlight、Java Fx 你方唱罢我登台,对 JavaScript 倒也可算“小打小闹”。
不管怎么样,JavaScript 就坚守在浏览器的阵地。Google Gmail 之于 JavaScript 所倚重的力量有目共睹,于是数以百计的项目纷纷把 JavaScript 派上前端。这一热潮更是催生了 JS Runtime 之竞争态势,Apple 的 Safari/Webkit、Mozilla 的 Firefox OS、连微软的 Metro 界面开发都把 HTML5 置于与 C#、VB 平起平坐的地位。
若说语言有生涯,这便是 JavaScript 生涯中的第一个转折点。
渐渐地,JavaScript 成为一门体面的语言。这固然与原发明者出色的设计理念有关;更重要的是,拜无处不在的浏览器所赐——JavaScript 比任何语言都有资格兑现了 Java 那古老的承诺“一次编写、到处运行”。
然而,没有下一站“给力”的开发潮流,恐怕一切美好都是“虚火”,不足以让人们把视野关注在 JavaScript 身上。如果把 JavaScript 比作一个男人,他会有第二个转折点吗?
不如将 JavaScript 真正可应用于服务端编程,岂不是更好!?这种全端的开发模式不用说也是顺理成章的。
——恰好,Node 出现了。
关于 Node 本身的优点已经铺天盖地了,不想多分析,但要探讨的是,Node 之出现对于 JavaScript 的“利导”不见得也是一帆风顺的。
JavaScript 设计的初衷是为了强化 Netscape 浏览器的展现能力,仅仅是脚本之目的。不曾想,现在业已成为多媒体、多任务、多内核网络世界中一员,然而微妙的却是,JavaScript 并没有摇身一变成为支持多线程的语言(也许随着语言规范的发展也会加入),而是稳固单线程的一门语言(早已超出脚本,可称为语言了,或者界限已经模糊了)。
下面我们花大量篇幅来介绍扫服务端事件驱动开发的概念。理解这些概念是实践 JS 异步编程的关键。
在许多语言中,事件模型不属于语言级别的支援,而是由外层 API 提供。但 JS 事件一直是语言的核心;
前面已经提到,JS 对闭包天然的支持。这姑且不以晦涩的闭包概念深入原理,只是明白
引入线程的概念是为了“并行”,可以处理多个任务同时开始,同时执行,同时进行,从而整体上加快最终效率。多个线程对应多项任务,多个对多个的分工这很好理解。但我们知道, Node JS 始终是单线程程序,却怎么运行多个任务,而且效率反而高?——这怎么说?实际上,我要告诉大家三点,1)JS 的确同一时间内只会做一件事,这是所谓单线程的表现;2)那岂不是同步意思了,难道不能并行了吗?但没关系,且看;3)我们把 JS 设计为一个“圈 Loop”,一个可以永远(当然也可以手工或者强行中止的)运行下去的循环,同时这个循环结构上是个队列,允许你不断往这个队列加入新任务。如果发现处理完毕的事件,则从队列中剔除表示执行完毕;如果没处理完毕,嗯~这个循环看了看之后不做什么,继续走下去不停留。又因为是循环的缘故,尚未完成的任务又会被事件机制访问,直到执行完毕为止。如此便可以把多个任务“不落单”地处理完毕。
是不是到这里,问题就完了?不对~感觉多任务执行如何哪里,你始终还没有说清楚,对不对?
嗯,能够提出深入的发问很好。尽管我没有挖据 V8 & Linux 代码去论证,但相信凭借我的自圆自说,个中的原理是这样的:
虽然 JS 初始化了底层去执行任务,但 JS 并不干涉任务的过程,不关心任务怎样完成,他只需要知道最终结果,ok (触发用户成功的回调)还是不 ok(触发 err 事件)。JS 知不知道这些多个线程存在?当然知道,但决不会把它们暴露出来,而是自己“事件循环”的机制来调度。你可以把 JS 这一层面想象为一个指挥者、总的调度者,它催生了多个任务同时跑,然后经常不辞劳苦地围着任务列表(即“事件队列”)在转,一个一个挨着问,“搞定没有”?“没有吗?”,“没有,在弄呢”,“好,我不催你”,继续访问下家……周而复始。这个过程中,系统事件内部是有多线程在跑的。只是我们不晓得而已,我们看到的只是 JS 层面的机制。这固然是 Node 优雅的地方,也是其卖点所在:不用线程却能操控多任务。也许不了解的人以为这是 JS 的“魔法”所赐,但无论如何形容,我们的任务就是要摸清楚这套 JS 机制,为我所用。
若要以线程称呼 JS,那么 JS 便是单线程程序。但有没有多线程的 JS?有!更确切地说,你可以在你程序中让多线程参与进来,但 JS 基本单元就只是一条线程。未来 JS 语言规范会出现线程处理的关键字,不是没有这种可能。但现在 JS 单线程模型是简单的、朴素的、友好的。当然有许多方式为你的 JS 程序提供多线程的支持。下面我们也分别进行介绍。假设不借助其他手段,一个 JS 程序有且只有一个事件循环队列。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Gone:', end -start, 'ms'); }, 200); // alert(11) while((new Date - start) < 1000){} // 强行阻塞 // alert(33)……未完待续……