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

异步之殇

NON-BLOCKING无限好?

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

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

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

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

require "em-files"

EM::run do
EM::File::open(__FILE__, "r") do |io|
io.read(1024) do |data|
puts data
io.close
end
EM::stop
end
end

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

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

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

fs.readdir(__dirname, function(e, files) {//callback 1
if(e) {
return console.log(e);
}
dirs.forEach(function(file) {//callback 2
fs.stat(file, function(e, stats) {//callback 3
if(e) {
return console.log(e);
}
if(stats.isFile()) {
console.log(stats.size);
}
});
});
});

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

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

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

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

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

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

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

解决方案

EVENTED

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

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

var TopicController = new EventEmitter();

TopicController.list = function() {//a simple wrap for ajax request
$.getJSON("/api/topics", this.notify("topic:list"));
return this;
}; TopicController.show = function(id) {//a simple wrap for ajax request
$.getJSON("/api/topics/" + id, this.notify("topic:show", id));
return this;
}; TopicController.bind = function() {//bind DOM events
$("#sexyButton").on("click", this.run.bind(this));
return this;
}; TopicController._queryTopic = function(data) {
var list = data.topics.map(function(t) {
return t.id + ". " + t.title + "/n";
});
var id = confirm("which topcis are you interested in? Select by ID : " + list);
this.show(id).listenTo("topic:show", this._showTopic);
}; TopicController._showTopic = function(data) {
alert(data.content);
}; TopicController.listenTo = function(eventName, listener) {//a helper method to `bind`
this.on(eventName, listener.bind(this));
}; TopicController.notify = function(eventName) {//generate a notify callback internally
var self = this, args;
args = Array.prototype.slice(arguments, 1);
return function(data) {
args.unshift(data);
args.unshift(eventName);
self.emit.apply(self, args);
};
}; TopicController.run = function() {
this.list().lisenTo("topic:list", this._queryTopic);
}; // kickoff
$(function() {
TopicController.run();
});

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

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

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

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

高阶函数

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

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

var runInSeries = function(ops, done) {
var i = 0, next;
next = function(e) {
if(e) {
return done(e);
}
var args = Array.prototype.slice.call(arguments, 1);
args.push(next);
ops[0].apply(null, args);
};
next();
};

还是我们之前的例子:

var list = function(next) {
$.getJSON("/api/topics", function(data) { next(null, data); });
}; var query = function(data, next) {
var list = data.topics.map(function(t) {
return t.id + ". " + t.title + "/n";
});
var id = confirm("which topcis are you interested in? Select by ID : " + list);
next(null, id);
}; var show = function(id, next) {
$.getJSON("/api/topics/" + id, function(data) { next(null, data); });
}; $("#sexyButton").on("click", function() {
runInSeries([list, query, show], function(e, detail) {
alert(detail);
});
});

看起来还是很不错的,简洁并且清晰,最终的代码量也没有增加。如果你喜欢这种方式,去看一下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来解决之前的那个小场景中的异步问题:

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

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

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

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

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

GENEARTOR

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

var Fiber = require('fibers');

function sleep(ms) {
var fiber = Fiber.current;
setTimeout(function() {
fiber.run();
}, ms);
Fiber.yield();
} Fiber(function() {
console.log('wait... ' + new Date);
sleep(1000);
console.log('ok... ' + new Date);
}).run();
console.log('back in main');

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

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

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

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

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

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

为了利用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. Qt使用MSVC编译器不能正确显示中文的解决方案

    用VisualStudio做为IDE,使用Qt框架,显示中文,会出现乱码的情况. 原因:MSVC编译器虽然可以正常编译带BOM的UTF-8编译的源文件,但是生成的可执行文件的编码是Windows本地字 ...

  2. null id in entry (don't flush the Session after an exception occurs)

    null id in entry (don't flush the Session after an exception occurs) 遇到这个异常实属不小心所致,最初看到异出的错误信息时我误认为是 ...

  3. 吴裕雄 python 数据处理(2)

    import pandas as pd data = pd.read_csv("F:\\python3_pachongAndDatareduce\\data\\pandas data\\hz ...

  4. 贝叶斯vs频率派:武功到底哪家强?| 说人话的统计学·协和八(转)

    回我们初次见识了统计学理论中的“独孤九剑”——贝叶斯统计学(戳这里回顾),它的起源便是大名鼎鼎的贝叶斯定理. 整个贝叶斯统计学的精髓可以用贝叶斯定理这一条式子来概括: 我们做数据分析,绝大多数情况下希 ...

  5. Flexvolume

    https://kubernetes.io/docs/concepts/storage/volumes/ https://github.com/kubernetes/community/blob/ma ...

  6. CSS 折角效果

    1 <style type="text/css"> .div1 { width: 200px; height: 200px; background-color: #ff ...

  7. LeanCloud

    [Nodejs 访问 LeanCloud] 代码中使用 SDK: var AV = require('avoscloud-sdk') AV.initialize('AppID', ''AppKey) ...

  8. nyoj743-复杂度 【排列组合】

    http://acm.nyist.net/JudgeOnline/problem.php?pid=743 复杂度 时间限制:1000 ms  |  内存限制:65535 KB 难度:3   描述 fo ...

  9. goim源码分析与二次开发-comet分析二

    这篇就是完全原版了,作为一个开始,先介绍comet入口文件main.go 第一步是初始化配置,还有白名单.还有性能监口,整体来说入口代码简洁可读性很强 然后开始初始化监控,还有bukcet这里buck ...

  10. Java Tomcat下载、安装和环境变量配置

    win10下Tomcat的下载.安装和环境变量的配置 -----made by siwuxie095                             1.首先到Tomcat官网,传送阵:点击开 ...