本文从异步风格讲起,分析Javascript中异步变成的技巧、问题和解决方案。具体的,从回调造成的问题说起,并谈到了利用事件、Promise、Generator等技术来解决这些问题。

异步之殇

NON-BLOCKING无限好?

异步,是没有线程模型的Javascript的救命稻草。说得高大上一些,就是运用了Reactor设计模式1

Javascript的一切都是围绕着“异步”二子的。无论是浏览器环境,还是node环境,大多数API都是通过“事件”来将请求(或消息、调用)和返回值(或结果)分离。而“事件”,都离不开回调(Callback),例如,

  1. var fs = require("fs");
  2. fs.readFile(__filename, function(e, data) {
  3. console.log("2. in callback");
  4. });
  5. console.log("1. after invoke");

fs模块封装了复杂的IO模块,其调用结果是通过一个简单的callback告诉调用者的。看起来是十分不错的,我们看看Ruby的EventMachine

  1. require "em-files"
  2. EM::run do
  3. EM::File::open(__FILE__, "r") do |io|
  4. io.read(1024) do |data|
  5. puts data
  6. io.close
  7. end
  8. EM::stop
  9. end
  10. end

由于Ruby的标准库里面的API全是同步的,异步的只有类似EventMachine这样的第三方API才能提供支持。实际风格上,两者类似,就我们这个例子来说,Javascript的版本似乎更加简介,而且不需要添加额外的第三方模块。

异步模式,相比线程模式,损耗更小,在部分场景性能甚至比Java更好2。并且,non-blocking的API是node默认的,这使nodejs和它的异步回调大量应用。

例如,我们想要找到当前目录中所有文件的尺寸:

  1. fs.readdir(__dirname, function(e, files) {//callback 1
  2. if(e) {
  3. return console.log(e);
  4. }
  5. dirs.forEach(function(file) {//callback 2
  6. fs.stat(file, function(e, stats) {//callback 3
  7. if(e) {
  8. return console.log(e);
  9. }
  10. if(stats.isFile()) {
  11. console.log(stats.size);
  12. }
  13. });
  14. });
  15. });

非常简单的一个任务便造成了3层回调。在node应用爆发的初期,大量的应用都是在这样的风格中诞生的。显然,这样的代码风格有如下风险:

  1. 代码难以阅读、维护:嵌套多层回调之后,作者自己都不清楚函数层次了。
  2. 潜在的调用堆栈消耗:Javascript中,远比你想像的简单去超出最大堆栈。不少第三方模块并没有做到异步调用,却装作支持回调,堆栈的风险就更大。
  3. 还想更遭么?前两条就够了……

不少程序员,因为第一条而放弃nodejs,甚至放弃Javascript。而关于第二条,各种隐性bug的排除和性能损耗的优化工作在向程序员招手。

等等,你说我一直再说node,没有提及浏览器中的情况?我们来看个例子:

  1. /*glboal $ */
  2. // we have jquery in the `window`
  3. $("#sexyButton").on("click", function(data) {//callback 1
  4. $.getJSON("/api/topcis", function(data) {//callback 2
  5. var list = data.topics.map(function(t) {
  6. return t.id + ". " + t.title + "/n";
  7. });
  8. var id = confirm("which topcis are you interested in? Select by ID : " + list);
  9. $.getJSON("/api/topics/" + id, function(data) {//callback 3
  10. alert("Detail topic: " + data.content);
  11. });
  12. });
  13. });

我们尝试获取一个文章列表,然后给予用户一些交互,让用户选择希望详细了解的一个文章,并继续获取文章详情。这个简单的例子,产生了3个回调。

事实上,异步的性质是Javascript语言本身的固有风格,跟宿主环境无关。所以,回调漫天飞造成的问题是Javascript语言的共性。

解决方案

EVENTED

Javascript程序员也许是最有创造力的一群程序员之一。对于回调问题,最终有了很多解决方案。最自然想到的,便是利用事件机制。

还是之前加载文章的场景:

  1. var TopicController = new EventEmitter();
  2. TopicController.list = function() {//a simple wrap for ajax request
  3. $.getJSON("/api/topics", this.notify("topic:list"));
  4. return this;
  5. };
  6. TopicController.show = function(id) {//a simple wrap for ajax request
  7. $.getJSON("/api/topics/" + id, this.notify("topic:show", id));
  8. return this;
  9. };
  10. TopicController.bind = function() {//bind DOM events
  11. $("#sexyButton").on("click", this.run.bind(this));
  12. return this;
  13. };
  14. TopicController._queryTopic = function(data) {
  15. var list = data.topics.map(function(t) {
  16. return t.id + ". " + t.title + "/n";
  17. });
  18. var id = confirm("which topcis are you interested in? Select by ID : " + list);
  19. this.show(id).listenTo("topic:show", this._showTopic);
  20. };
  21. TopicController._showTopic = function(data) {
  22. alert(data.content);
  23. };
  24. TopicController.listenTo = function(eventName, listener) {//a helper method to `bind`
  25. this.on(eventName, listener.bind(this));
  26. };
  27. TopicController.notify = function(eventName) {//generate a notify callback internally
  28. var self = this, args;
  29. args = Array.prototype.slice(arguments, 1);
  30. return function(data) {
  31. args.unshift(data);
  32. args.unshift(eventName);
  33. self.emit.apply(self, args);
  34. };
  35. };
  36. TopicController.run = function() {
  37. this.list().lisenTo("topic:list", this._queryTopic);
  38. };
  39. // kickoff
  40. $(function() {
  41. TopicController.run();
  42. });

可以看到,现在这种写法B格就高了很多。各种封装、各种解藕。首先,除了万能的jQuery,我们还依赖EventEmitter,这是一个观察者模式的实现3,比如asyncly/EventEmitter2。简单的概括一下这种风格:

  1. 杜绝了大部分将匿名函数用作回调的场景,达到零嵌套,代码简介明了
  2. 每个状态(或步骤)之间,利用事件机制进行关联
  3. 每个步骤都相互独立,方便日后维护

如果你硬要挑剔的话,也有缺点;

  1. 由于过度分离,整体流程模糊
  2. 代码量激增,又加大了另一种维护成本

高阶函数

利用高阶函数,可以顺序、并发的将函数递归执行。

我们可以编写一个高阶函数,让传入的函数顺序执行:

  1. var runInSeries = function(ops, done) {
  2. var i = 0, next;
  3. next = function(e) {
  4. if(e) {
  5. return done(e);
  6. }
  7. var args = Array.prototype.slice.call(arguments, 1);
  8. args.push(next);
  9. ops[0].apply(null, args);
  10. };
  11. next();
  12. };

还是我们之前的例子:

  1. var list = function(next) {
  2. $.getJSON("/api/topics", function(data) { next(null, data); });
  3. };
  4. var query = function(data, next) {
  5. var list = data.topics.map(function(t) {
  6. return t.id + ". " + t.title + "/n";
  7. });
  8. var id = confirm("which topcis are you interested in? Select by ID : " + list);
  9. next(null, id);
  10. };
  11. var show = function(id, next) {
  12. $.getJSON("/api/topics/" + id, function(data) { next(null, data); });
  13. };
  14. $("#sexyButton").on("click", function() {
  15. runInSeries([list, query, show], function(e, detail) {
  16. alert(detail);
  17. });
  18. });

看起来还是很不错的,简洁并且清晰,最终的代码量也没有增加。如果你喜欢这种方式,去看一下caolan/async会发现更多精彩。

PROMISE

A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.

除开文绉绉的解释,Promise是一种对一个任务的抽象。Promise的相关API提供了一组方法和对象来实现这种抽象。

Promise的实现目前有很多:

虽然标准很多,但是所有的实现基本遵循如下基本规律:

  • Promise对象

    • 是一个有限状态机

      • 完成(fulfilled)
      • 否定(rejected)
      • 等待(pending)
      • 结束(settled)
    • 一定会有一个then([fulfill], [reject])方法,让使用者分别处理成功失败
    • 可选的done([fn])fail([fn])方法
    • 支持链式API
  • Deffered对象
    • 提供rejectresolve方法,来完成一个Promise

笔者会在专门的文章内介绍Promise的具体机制和实现。在这里仅浅尝辄止,利用基本随处可得的jQuery来解决之前的那个小场景中的异步问题:

  1. $("#sexyButton").on("click", function(data) {
  2. $.getJSON("/api/topcis").done(function(data) {
  3. var list = data.topics.map(function(t) {
  4. return t.id + ". " + t.title + "/n";
  5. });
  6. var id = confirm("which topcis are you interested in? Select by ID : " + list);
  7. $.getJSON("/api/topics/" + id).done(function(done) {
  8. alert("Detail topic: " + data.content);
  9. });
  10. });
  11. });

很遗憾,使用Promise并没有让回调的问题好多少。在这个场景,Promise的并没有体现出它的强大之处。我们把jQuery官方文档中的例子拿出来看看:

  1. $.when( $.ajax( "/page1.php" ), $.ajax( "/page2.php" ) ).done(function( a1, a2 ) {
  2. // a1 and a2 are arguments resolved for the page1 and page2 ajax requests, respectively.
  3. // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
  4. var data = a1[ 0 ] + a2[ 0 ]; // a1[ 0 ] = "Whip", a2[ 0 ] = " It"
  5. if ( /Whip It/.test( data ) ) {
  6. alert( "We got what we came for!" );
  7. }
  8. });

这里,同时发起了两个AJAX请求,并且将这两个Promise合并成一个,开发者只用处理这最终的一个Promise。

例如Q.jswhen.js的第三方库,可以支持更多复杂的特性。也会让你的代码风格大为改观。可以说,Promise为处理复杂流程开启了新的大门,但是也是有成本的。这些复杂的封装,都有相当大的开销6

GENEARTOR

ES6的Generator引入的yield表达式,让流程控制更加多变。node-fiber让我们看到了coroutine在Javascript中的样子。

  1. var Fiber = require('fibers');
  2. function sleep(ms) {
  3. var fiber = Fiber.current;
  4. setTimeout(function() {
  5. fiber.run();
  6. }, ms);
  7. Fiber.yield();
  8. }
  9. Fiber(function() {
  10. console.log('wait... ' + new Date);
  11. sleep(1000);
  12. console.log('ok... ' + new Date);
  13. }).run();
  14. console.log('back in main');

但想象一下,如果每个Javascript都有这个功能,那么一个正常Javascript程序员的各种尝试就会被挑战。你的对象会莫名其妙的被另外一个fiber中的代码更改。

也就是说,还没有一种语法设计能让支持fiber和不支持fiber的Javascript代码混用并且不造成混淆。node-fiber的这种不可移植性,让coroutine在Javascript中并不那么现实7

但是yield是一种Shallow coroutines,它只能停止用户代码,并且只有在GeneratorFunction才可以用yield

笔者在另外一篇文章中已经详细介绍了如何利用Geneator来解决异步流程的问题。

利用yield实现的suspend方法,可以让我们之前的问题解决的非常简介:

  1. $("#sexyButton").on("click", function(data) {
  2. suspend(function *() {
  3. var data = yield $.getJSON("/api/topcis");
  4. var list = data.topics.map(function(t) {
  5. return t.id + ". " + t.title + "/n";
  6. });
  7. var id = confirm("which topcis are you interested in? Select by ID : " + list);
  8. var detail = yield $.getJSON("/api/topics/");
  9. alert("Detail topic: " + detail.content);
  10. })();
  11. });

为了利用yield,我们也是有取舍的:

  1. Generator的兼容性并不好,仅有新版的node和Chrome支持
  2. 需要大量重写基础框架,是接口规范化(thunkify),来支持yield的一些约束
  3. yield所产生的代码风格,可能对部分新手造成迷惑
  4. 多层yield所产生堆栈及其难以调试

结语

说了这么多,异步编程这种和线程模型迥然不同的并发处理方式,随着node的流行也让更多程序员了解其与众不同的魅力。如果下次再有C或者Java程序员说,Javascript的回调太难看,请让他好好读一下这篇文章吧!

原文:http://hao.jser.com/archive/4296/


ASYNC PROGRAMING IN JAVASCRIPT[转]的更多相关文章

  1. Async/Await是这样简化JavaScript代码的

    译者按: 在Async/Await替代Promise的6个理由中,我们比较了两种不同的异步编程方法:Async/Await和Promise,这篇博客将通过示例代码介绍Async/Await是如何简化J ...

  2. 浏览器环境下JavaScript脚本加载与执行探析之defer与async特性

    defer和async特性相信是很多JavaScript开发者"熟悉而又不熟悉"的两个特性,从字面上来看,二者的功能很好理解,分别是"延迟脚本"和"异 ...

  3. JavaScript async/await:优点、陷阱及如何使用

    翻译练习 原博客地址:JavaScript async/await: The Good Part, Pitfalls and How to Use ES7中引进的async/await是对JavaSc ...

  4. 每个JavaScript开发人员应该知道的33个概念

    每个JavaScript开发人员应该知道的33个概念 介绍 创建此存储库的目的是帮助开发人员在JavaScript中掌握他们的概念.这不是一项要求,而是未来研究的指南.它基于Stephen Curti ...

  5. 如何避免 await/async 地狱

    原文地址:How to escape async/await hell 译文出自:夜色镇歌的个人博客 async/await 把我们从回调地狱中解救了出来,但是如果滥用就会掉进 async/await ...

  6. Generator和Async

    引言 接触过Ajax请求的会遇到过异步调用的问题,为了保证调用顺序的正确性,一般我们会在回调函数中调用,也有用到一些新的解决方案如Promise相关的技术. 在异步编程中,还有一种常用的解决方案,它就 ...

  7. 《JavaScript高级程序设计(第3版)》阅读总结记录第二章之在HTML中使用JavaScript

    本章目录: 2.1 <script> 元素 2.1.1 标签的位置 2.1.2 延迟脚本 2.1.3 异步脚本 2.1.4 在XHTML 中的用法 2.1.5 不推荐使用的语法 2.2 嵌 ...

  8. 8张图让你一步步看清 async/await 和 promise 的执行顺序

    摘要: 面试必问 原文:8张图帮你一步步看清 async/await 和 promise 的执行顺序 作者:ziwei3749 Fundebug经授权转载,版权归原作者所有. 为什么写这篇文章? 说实 ...

  9. 最棒的 JavaScript 学习指南(2018版)

    译者注:原文作者研究了近2.4万篇 JavaScript 文章得出这篇总结,全文包含学习指南.新人上手.Webpack.性能.基础概念.函数式编程.面试.教程案例.Async Await.并发.V8. ...

随机推荐

  1. 通过maven 上传jar 到nexus3,cong nexus3下载jar

    nexus是一种常见的maven私服软件. 网上介绍的都是nexus2的使用,下面是最新版nexus3的使用方式. 首先需要从官网下载nexus3的包,很卡. 下载好以后解压会有两个文件夹:nexus ...

  2. Mongodb 折腾笔记

    简介: Mongodb 是一个由 C++ 语言编写的基于分布式文件存储的数据库,是目前最像关系型数据库的非关系型数据库. 下载地址:https://fastdl.mongodb.org/linux/m ...

  3. Linux下生成openssl自签名证书

    校验证书是否被 CA 证书签名,正确的情况: $ openssl verify -CAfile /etc/kubernetes/cert/ca.pem /etc/kubernetes/cert/kub ...

  4. Linux启动提示Kernel panic - not syncing: Attempted to kill init解决办法

    系统类型:CentOS 6.5(x64) 启动提示:Kernel panic - not syncing: Attempted to kill init 解决办法: 系统启动的时候,按下‘e’键进入g ...

  5. eclipse 使用tomcat7.0建立Dynamic Web Project 时 web.xml的问题

    最近使用Eclipse helios版本结合tomcat7.0建立动态的web项目时,发现在WEB-INF下的web.xml没有了. 解决方案: 建立web项目时,建到第三个下一步时,将 Genera ...

  6. 迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

    大凡出名的MVC,MVVM框架都有todo例子,我们也搞一下看看avalon是否这么便宜. 我们先从react的todo例子中扒一下HTML与CSS用用. <!doctype html> ...

  7. hadoop 官方配置文件解析

    比如我的版本是2.8.4 官网文档是: http://hadoop.apache.org/docs/r2.8.4/ 基本配置文件:包括一般的端口 hdfs-default.xml dfs.nameno ...

  8. nodejs 与 json

    nodeJs读取文件(readfile) j json 处理: var fileData = fs.readFileSync(file);if (fileData) { var j = {}; cal ...

  9. ionic2

    拨打电话: <access origin="tel:*" launch-external="yes" /> 发邮件: <access orig ...

  10. ASP.NET中的URL编码解码(转)

    在对URL进行编码时,该用哪一个?这两都使用上有什么区别吗?测试: string file="文件上(传)篇.doc";string Server_UrlEncode=Server ...