探索Javascript异步编程-阿里云开发者社区

开发者社区> 行者武松> 正文

探索Javascript异步编程

简介:
+关注继续查看

异步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript越来越广的被使用,大量的异步IO操作使得该问题变得明显。许多不同的方法都可以解决这个问题,本文讨论了一些方法,但并不深入。大家需要根据自己的情况选择一个适于自己的方法。

探索Javascript异步编程

笔者在之前的一片博客中简单的讨论了Python和Javascript的异同,其实作为一种编程语言Javascript的异步编程是一个非常值得讨论的有趣话题。

JavaScript 异步编程简介

回调函数和异步执行

所谓的异步指的是函数的调用并不直接返回执行的结果,而往往是通过回调函数异步的执行。

我们先看看回调函数是什么:


  1. var fn = function(callback) { 
  2.     // do something here 
  3.     ... 
  4.     callback.apply(this, para); 
  5. }; 
  6.    
  7. var mycallback = function(parameter) { 
  8.     // do someting in customer callback 
  9. }; 
  10.    
  11. // call the fn with callback as parameter 
  12. fn(mycallback); 

回调函数,其实就是调用用户提供的函数,该函数往往是以参数的形式提供的。回调函数并不一定是异步执行的。比如上述的例子中,回调函数是被同步执行的。大部分语言都支持回调,C++可用通过函数指针或者回调对象,Java一般也是使用回调对象。

在Javascript中有很多通过回调函数来执行的异步调用,例如setTimeout()或者setInterval()。


  1. setTimeout(function(){ 
  2.     console.log("this will be exectued after 1 second!"); 
  3. },1000); 

在 以上的例子中,setTimeout直接返回,匿名函数会在1000毫秒(不一定能保证是1000毫秒)后异步触发并执行,完成打印控制台的操作。也就是 说在异步操作的情境下,函数直接返回,把控制权交给回调函数,回调函数会在以后的某一个时间片被调度执行。那么为什么需要异步呢?为什么不能直接在当前函 数中完成操作呢?这就需要了解Javascript的线程模型了。

Javascript线程模型和事件驱动

Javascript 最初是被设计成在浏览器中辅助提供HTML的交互功能。在浏览器中都包含一个Javascript引擎,Javscript程序就运行在这个引擎之中,并 且只有一个线程。单线程能都带来很多优点,程序员们可以很开心的不用去考虑诸如资源同步,死锁等多线程阻塞式编程所需要面对的恼人的问题。但是很多人会 问,既然Javascript是单线程的,那它又如何能够异步的执行呢?

这 就需要了解到Javascript在浏览器中的事件驱动(event driven)机制。事件驱动一般通过事件循环(event loop)和事件队列(event queue)来实现的。假定浏览器中有一个专门用于事件调度的实例(该实例可以是一个线程,我们可以称之为事件分发线程event dispatch thread),该实例的工作就是一个不结束的循环,从事件队列中取出事件,处理所有很事件关联的回调函数(event handler)。注意回调函数是在Javascript的主线程中运行的,而非事件分发线程中,以保证事件处理不会发生阻塞。

Event Loop Code:


  1. while(true) { 
  2.  var event = eventQueue.pop(); 
  3.  if(event && event.handler) { 
  4.      event.handler.execute(); // execute the callback in Javascript thread 
  5.  } else { 
  6.      sleep(); //sleep some time to release the CPU do other stuff 
  7.  } 

通过事件驱动机制,我们可以想象Javascript的编程模型就是响应一系列的事件,执行对应的回调函数。很多UI框架都采用这样的模型(例如Java Swing)。

那为什要异步呢,同步不是很好么?

异步的主要目的是处理非阻塞,在和HTML交互的过程中,会需要一些IO操作(典型的就是Ajax请求,脚本文件加载),如果这些操作是同步的,就会阻塞其它操作,用户的体验就是页面失去了响应。

综上所述Javascript通过事件驱动机制,在单线程模型下,以异步回调函数的形式来实现非阻塞的IO操作。

Javascript异步编程带来的挑战

Javascript的单线程模型有很多好处,但同时也带来了很多挑战。

代码可读性

想象一下,如果某个操作需要经过多个非阻塞的IO操作,每一个结果都是通过回调,程序有可能会看上去像这个样子。


  1. operation1(function(err, result) { 
  2.     operation2(function(err, result) { 
  3.         operation3(function(err, result) { 
  4.             operation4(function(err, result) { 
  5.                 operation5(function(err, result) { 
  6.                     // do something useful 
  7.                 }) 
  8.             }) 
  9.         }) 
  10.     }) 
  11. }) 

我们称之为意大利面条式(spaghetti)的代码。这样的代码很难维护。这样的情况更多的会发生在server side的情况下。

流程控制

异步带来的另一个问题是流程控制,举个例子,我要访问三个网站的内容,当三个网站的内容都得到后,合并处理,然后发给后台。代码可以这样写:


  1. var urls = ['url1','url2','url3']; 
  2. var result = []; 
  3.    
  4. for (var i = 0, len = urls.length(); i < len; i++ ) { 
  5.     $.ajax({ 
  6.         url: urls[i], 
  7.         context: document.body, 
  8.         success: function(){ 
  9.           //do something on success 
  10.           result.push("one of the request done successfully"); 
  11.           if (result.length === urls.length()) { 
  12.               //do something when all the request is completed successfully 
  13.           } 
  14.         }}); 

上述代码通过检查result的长度的方式来决定是否所有的请求都处理完成,这是一个很丑陋方法,也很不可靠。

异常和错误处理

通过上一个例子,我们还可以看出,为了使程序更健壮,我们还需要加入异常处理。 在异步的方式下,异常处理分布在不同的回调函数中,我们无法在调用的时候通过try…catch的方式来处理异常, 所以很难做到有效,清楚。

更好的Javascript异步编程方式

“这是最好的时代,也是最糟糕的时代”

为了解决Javascript异步编程带来的问题,很多的开发者做出了不同程度的努力,提供了很多不同的解决方案。然而面对如此众多的方案应该如何选择呢?我们这就来看看都有哪些可供选择的方案吧。

Promise

Promise 对 象曾经以多种形式存在于很多语言中。这个词最先由C++工程师用在Xanadu 项目中,Xanadu 项目是Web 应用项目的先驱。随后Promise 被用在E编程语言中,这又激发了Python 开发人员的灵感,将它实现成了Twisted 框架的Deferred 对象。

2007 年,Promise 赶上了JavaScript 大潮,那时Dojo 框架刚从Twisted框架汲取灵感,新增了一个叫做dojo.Deferred 的对象。也就在那个时候,相对成熟的Dojo 框架与初出茅庐的jQuery 框架激烈地争夺着人气和名望。2009 年,Kris Zyp 有感于dojo.Deferred 的影响力提出了CommonJS 之Promises/A 规范。同年,Node.js 首次亮相。

在 编程的概念中,future,promise,和delay表示同一个概念。Promise翻译成中文是“承诺”,也就是说给你一个东西,我保证未来能够 做到,但现在什么都没有。它用来表示异步操作返回的一个对象,该对象是用来获取未来的执行结果的一个代理,初始值不确定。许多语言都有对Promise的 支持。

Promise的核心是它的then方法,我们可以使用这个方法从异步操作中得到返回值,或者是异常。then有两个可选参数(有的实现是三个),分别处理成功和失败的情景。


  1. var promise = doSomethingAync() 
  2. promise.then(onFulfilled, onRejected) 

异 步调用doSomethingAync返回一个Promise对象promise,调用promise的then方法来处理成功和失败。这看上去似乎并没 有很大的改进。仍然需要回调。但是和以前的区别在于,首先异步操作有了返回值,虽然该值只是一个对未来的承诺;其次通过使用then,程序员可以有效的控 制流程异常处理,决定如何使用这个来自未来的值。

对于嵌套的异步操作,有了Promise的支持,可以写成这样的链式操作:


  1. operation1().then(function (result1) { 
  2.     return operation2(result1) 
  3. }).then(function (result2) { 
  4.     return operation3(result2); 
  5. }).then(function (result3) { 
  6.     return operation4(result3); 
  7. }).then(function (result4) { 
  8.     return operation5(result4) 
  9. }).then(function (result5) { 
  10.     //And so on 
  11. }); 

Promise提供更便捷的流程控制,例如Promise.all()可以解决需要并发的执行若干个异步操作,等所有操作完成后进行处理。


  1. var p1 = async1(); 
  2. var p2 = async2(); 
  3. var p3 = async3(); 
  4. Promise.all([p1,p2,p3]).then(function(){ 
  5.     // do something when all three asychronized operation finished 
  6. }); 

对于异常处理,


  1. doA() 
  2.   .then(doB) 
  3.   .then(null,function(error){ 
  4.       // error handling here 
  5.   }) 

如果doA失败,它的Promise会被拒绝,处理链上的下一个onRejected会被调用,在这个例子中就是匿名函数function(error){}。比起原始的回调方式,不需要在每一步都对异常进行处理。这生了不少事。

以上只是对于Promise概念的简单陈述,Promise拥有许多不同规范建议(A,A+,B,KISS,C,D等),名字(Future,Promise,Defer),和开源实现。大家可以参考一下的这些链接。

如果你有选择困难综合症,面对这么多的开源库不知道如何决断,先不要急,这还只是一部分,还有一些库没有或者不完全采用Promise的概念

Non-Promise

下面列出了其它的一些开源的库,也可以帮助解决Javascript中异步编程所遇到的诸多问题,它们的解决方案各不相同,我这里就不一一介绍了。大家有兴趣可以去看看或者试用一下。

Non-3rd Party

其实,为了解决Javascript异步编程带来的问题,不一定非要使用Promise或者其它的开源库,这些库提供了很好的模式,但是你也可以通过有针对性的设计来解决。

比如,对于层层回调的模式,可以利用消息机制来改写,假定你的系统中已经实现了消息机制,你的code可以写成这样:


  1. var co = require('co'); 
  2. var fs = require('fs'); 
  3.    
  4. var stat = function(path) { 
  5.   return function(cb){ 
  6.     fs.stat(path,cb); 
  7.   } 
  8. }; 
  9.    
  10. var readFile = function(filename) { 
  11.   return function(cb){ 
  12.     fs.readFile(filename,cb); 
  13.   } 
  14. }; 
  15.    
  16. co(function *() { 
  17.   var stat = yield stat('./README.md'); 
  18.   var content = yield readFile('./README.md'); 
  19. })(); 

这样我们就把嵌套的异步调用,改写成了顺序执行的事件处理。

更多的方式,请大家参考这篇文章,它提出了解决异步的五种模式:回调、观察者模式(事件)、消息、Promise和有限状态机(FSM)。

下一代Javscript对异步编程的增强

ECMAScript6

下一代的Javascript标准Harmony,也就是ECMAScript6正在酝酿中,它提出了许多新的语言特性,比如箭头函数、类(Class)、生成器(Generator)、Promise等等。其中Generator和Promise都可以被用于对异步调用的增强。

Nodejs的开发版V0.11已经可以支持ES6的一些新的特性,使用node –harmony命令来运行对ES6的支持。

co、Thunk、Koa

koa是由Express原班人马(主要是TJ)打造,希望提供一个更精简健壮的nodejs框架。koa依赖ES6中的Generator等新特性,所以必须运行在相应的Nodejs版本上。

利用Generator、coThunk,可以在Koa中有效的解决Javascript异步调用的各种问题。

co是一个异步流程简化的工具,它利用Generator把一层层嵌套的调用变成同步的写法。


  1. var co = require('co'); 
  2. var fs = require('fs'); 
  3.    
  4. var stat = function(path) { 
  5.   return function(cb){ 
  6.     fs.stat(path,cb); 
  7.   } 
  8. }; 
  9.    
  10. var readFile = function(filename) { 
  11.   return function(cb){ 
  12.     fs.readFile(filename,cb); 
  13.   } 
  14. }; 
  15.    
  16. co(function *() { 
  17.   var stat = yield stat('./README.md'); 
  18.   var content = yield readFile('./README.md'); 
  19. })(); 

通过co可以把异步的fs.readFile当成同步一样调用,只需要把异步函数fs.readFile用闭包的方式封装。

利用Thunk可以进一步简化为如下的code, 这里Thunk的作用就是用闭包封装异步函数,返回一个生成函数的函数,供生成器来调用。

利用co可以串行或者并行的执行异步调用。


  1. var thunkify = require('thunkify'); 
  2. var co = require('co'); 
  3. var fs = require('fs'); 
  4.    
  5. var stat = thunkify(fs.stat); 
  6. var readFile = thunkify(fs.readFile); 
  7.    
  8. co(function *() { 
  9.   var stat = yield stat('./README.md'); 
  10.   var content = yield readFile('./README.md'); 
  11. })(); 

串行


  1. co(function *() { 
  2.   var a = yield request(a); 
  3.   var b = yield request(b); 
  4. })(); 

并行


  1. co(function *() { 
  2.  var res = yield [request(a), request(b)]; 
  3. })(); 

更多详细的内容,大家可以参考这两篇文章12

总结

异 步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript越来越广的被使用,大量的异步IO操作使得该问题变得明 显。许多不同的方法都可以解决这个问题,本文讨论了一些方法,但并不深入。大家需要根据自己的情况选择一个适于自己的方法。

同时,随着ES6的定义,Javascript的语法变得越来越丰富,更多的功能带来了很多便利,然而原本简洁,单一目的的Javascript变得复杂,也要承担更多的任务。Javascript何去何从,让我们拭目以待。


来源:51CTO

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
《JavaScript面向对象编程指南》——导读
JavaScript面向对象编程指南 这本书旨在介绍JavaScript语言,这是一种具有高度表达能力的、基于原型特性的、非常灵活的面向对象程序设计语言。只要我们摒弃之前设计师所做的那种类似翻滚按钮的玩具思路,这种有趣的、独特的语言就会重新焕发活力,并且远胜往昔,今天的Web 2.0世界中的AJAX、胖客户端程序设计、丰富的仿桌面型网络应用程序、缩放式地图以及基于Web的邮件客户端基本上都依赖于JavaScript所带来的高交互性用户体验。
2106 0
JavaScript 编程精解 中文第三版 二、程序结构
二、程序结构 原文:Program Structure 译者:飞龙 协议:CC BY-NC-SA 4.
1249 0
JavaScript高级编程II
     原文地址: http://www.onlamp.com/pub/a/onlamp/2007/08/23/advanced-javascript-ii.html?page=1        在前面的文章中,我们介绍了两类JavaScript小工具及其源码:浮动文本和弹出菜单。
769 0
JavaScript 编程精解 中文第三版 六、对象的秘密
六、对象的秘密 原文:The Secret Life of Objects 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了《JavaScript 编程精解(第 2 版)》 抽象数据类型是通过编写一种特殊的程序来实现的,该程序根据可在其上执行的操作来定义类型。
1114 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4216 0
Javascript编程“陷阱”总结
javascript中有很多陷阱,现在来总结一下:
1403 0
Java异步编程之Future、CompletableFuture
本文从后端面临的几个压力开始讲起,分析了异步为什么可以提高性能、提高资源利用率,然后通过代码示例,介绍了Java对异步的API级别的支持,如Java5就出现的Future,以及Java8增强的CompletableFuture,最后,我们结合了Java Stream,完成了一个综合的异步应用。
365 0
+关注
行者武松
杀人者,打虎武松也。
14545
文章
2569
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载