众所周知(这也忒夸张了吧?),Javascript通过事件驱动机制,在单线程模型下,以异步的形式来实现非阻塞的IO操作。这种模式使得JavaScript在处理事务时非常高效,但这带来了很多问题,比如异常处理困难、函数嵌套过深。下面介绍几种目前已知的实现异步操作的解决方案。

一、回调函数

这是最古老的一种异步解决方案:通过参数传入回调,未来调用回调时让函数的调用者判断发生了什么。

直接偷懒上阮大神的例子:

假定有两个函数f1和f2,后者等待前者的执行结果。

如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。

  1. function f1(callback){
  2.     setTimeout(function () {
  3.       // f1的任务代码
  4.       callback();
  5.     }, 1000);
  6.   }

执行代码就变成下面这样:

f1(f2);

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱.也许你觉得上面的流程还算清晰。那是因为我等初级菜鸟还没见过世面,试想在前端领域打怪升级的过程中,遇到了下面的代码:

  1. doA(function(){
  2. doB();
  3. doC(function(){
  4. doD();
  5. })
  6. doE();
  7. });
  8. doF();

要想理清上述代码中函数的执行顺序,还真得停下来分析很久,正确的执行顺序是doA->doF->doB->doC->doE->doD.

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,程序的流程会很混乱,而且每个任务只能指定一个回调函数。

二、事件发布/订阅模式(观察者模式)

事件监听模式是一种广泛应用于异步编程的模式,是回调函数的事件化,任务的执行不取决于代码的顺序,而取决于某个事件是否发生。这种设计模式常被成为发布/订阅模式或者观察者模式。

浏览器原生支持事件,如Ajax请求获取响应、与DOM的交互等,这些事件天生就是异步执行的。在后端的Node环境中也自带了events模块,Node中事件发布/订阅的模式及其简单,使用事件发射器即可,示例代码如下:

  1. //订阅
  2. emitter.on("event1",function(message){
  3. console.log(message);
  4. });
  5. //发布
  6. emitter.emit('event1',"I am message!");

我们也可以自己实现一个事件发射器,代码实现参考了《JavaScript设计模式与开发实践》

  1. var event={
  2. clientList:[],
  3. listen:function (key,fn) {
  4. if (!this.clientList[key]) {
  5. this.clientList[key]=[];
  6. }
  7. this.clientList[key].push(fn);//订阅的消息添加进缓存列表
  8. },
  9. trigger:function(){
  10. var key=Array.prototype.shift.call(arguments),//提取第一个参数为事件名称
  11. fns=this.clientList[key];
  12. if (!fns || fns.length===0) {//如果没有绑定对应的消息
  13. return false;
  14. }
  15. for (var i = 0,fn;fn=fns[i++];) {
  16. fn.apply(this,arguments);//带上剩余的参数
  17. }
  18. },
  19. remove:function(key,fn){
  20. var fns=this.clientList[key];
  21. if (!fns) {//如果key对应的消息没人订阅,则直接返回
  22. return false;
  23. }
  24. if (!fn) {//如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
  25. fns&&(fns.length=0);
  26. }else{
  27. for (var i = fns.length - 1; i >= 0; i--) {//反向遍历订阅的回调函数列表
  28. var _fn=fns[i];
  29. if (_fn===fn) {
  30. fns.splice(i,1);//删除订阅者的回调函数
  31. }
  32. }
  33. }
  34. }
  35. };

只有这个事件订阅发布对象没有多大作用,我们要做的是给任意的对象都能添加上发布-订阅的功能:

在ES6中可以使用Object.assign(target,source)方法合并对象功能。如果不支持ES6可以自行设计一个拷贝函数如下:

  1. var installEvent=function(obj){
  2. for(var i in event){
  3. if(event.hasOwnProperty(i))
  4. obj[i]=event[i];
  5. }
  6. };

上述的函数就能给任意对象添加上事件发布-订阅功能。下面我们测试一下,假如你家里养了一只喵星人,现在它饿了。

  1. var Cat={};
  2. //Object.assign(Cat,event);
  3. installEvent(Cat);
  4. Cat.listen('hungry',function(){
  5. console.log("铲屎的,快把朕的小鱼干拿来!")
  6. });
  7. Cat.trigger('hungry');//铲屎的,快把朕的小鱼干拿来!

自定义发布-订阅模式介绍完了。

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

三、使用Promise对象

ES6标准中实现的Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

所谓Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件,并且这个事件提供统一的API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

下面以一个Ajax请求为例,Cnode社区的API中有这样一个流程,首先根据accesstoken获取用户名,然后可以根据用户名获取用户收藏的主题,如果我们想得到某个用户收藏的主题数量就要进行两次请求。如果不使用Promise对象,以Jquery的ajax请求为例:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Promise</title>
  6. </head>
  7. <body>
  8. </body>
  9. <script type="text/javascript" src="http://apps.bdimg.com/libs/jquery/1.7.2/jquery.min.js"></script>
  10. <script type="text/javascript">
  11. $.post("https://cnodejs.org/api/v1/accesstoken",{
  12. accesstoken:"XXXXXXXXXXXXXXXXXXXXXXXXXXX"
  13. },function (res1) {
  14. $.get("https://cnodejs.org/api/v1/topic_collect/"+res1.loginname,function(res2){
  15. alert(res2.data.length);
  16. });
  17. });
  18. </script>
  19. </html>

从上述代码中可以看出,两次请求相互嵌套,如果改成用Promise对象实现:

  1. function post(url,para){
  2. return new Promise(function(resolve,reject){
  3. $.post(url,para,resolve);
  4. });
  5. }
  6. function get(url,para){
  7. return new Promise(function(resolve,reject){
  8. $.get(url,para,resolve);
  9. });
  10. }
  11. var p1=post("https://cnodejs.org/api/v1/accesstoken",{
  12. accesstoken:"XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  13. });
  14. var p2=p1.then(function(res){
  15. return get("https://cnodejs.org/api/v1/topic_collect/"+res.loginname,{});
  16. });
  17. p2.then(function(res){
  18. alert(res.data.length);
  19. });

可以看到前面代码中的嵌套被解开了,(也许有人会说,这代码还变长了,坑爹吗这是,请不要在意这些细节,这里仅举例说明)。关于Promise对象的具体用法还有很多知识点,建议查找相关资料深入阅读,这里仅介绍它作为异步编程的一种解决方案。

四、使用Generator函数

关于Generator函数的概念可以参考阮大神的ES6标准入门,Generator可以理解为可在运行中转移控制权给其他代码,并在需要的时候返回继续执行的函数,看下面一个简单的例子:

  1. function* helloWorldGenerator(){
  2. yield 'hello';
  3. yield 'world';
  4. yield 'ending';
  5. }
  6. var hw=helloWorldGenerator();
  7. console.log(hw.next());
  8. console.log(hw.next());
  9. console.log(hw.next());
  10. console.log(hw.next());
  11. // { value: 'hello', done: false }
  12. // { value: 'world', done: false }
  13. // { value: 'ending', done: false }
  14. // { value: undefined, done: true }

Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。

Generator函数的暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

  1. step1(function (value1) {
  2. step2(value1, function(value2) {
  3. step3(value2, function(value3) {
  4. step4(value3, function(value4) {
  5. // Do something with value4
  6. });
  7. });
  8. });
  9. });

采用Promise改写上面的代码。(下面的代码使用了Promise的函数库Q)

  1. Q.fcall(step1)
  2. .then(step2)
  3. .then(step3)
  4. .then(step4)
  5. .then(function (value4) {
  6. // Do something with value4
  7. }, function (error) {
  8. // Handle any error from step1 through step4
  9. })
  10. .done();

上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量Promise的语法。Generator函数可以进一步改善代码运行流程。

  1. function* longRunningTask() {
  2. try {
  3. var value1 = yield step1();
  4. var value2 = yield step2(value1);
  5. var value3 = yield step3(value2);
  6. var value4 = yield step4(value3);
  7. // Do something with value4
  8. } catch (e) {
  9. // Handle any error from step1 through step4
  10. }
  11. }

如果只有Generator函数,任务并不会自动执行,因此需要再编写一个函数,按次序自动执行所有步骤。

  1. scheduler(longRunningTask());
  2. function scheduler(task) {
  3. setTimeout(function() {
  4. var taskObj = task.next(task.value);
  5. // 如果Generator函数未结束,就继续调用
  6. if (!taskObj.done) {
  7. task.value = taskObj.value
  8. scheduler(task);
  9. }
  10. }, 0);
  11. }

五、使用async函数

在ES7(还未正式标准化)中引入了Async函数的概念,async函数的实现就是将Generator函数和自动执行器包装在一个函数中。如果把上面Generator实现异步的操作改成async函数,代码如下:

  1. async function longRunningTask() {
  2. try {
  3. var value1 = await step1();
  4. var value2 = await step2(value1);
  5. var value3 = await step3(value2);
  6. var value4 = await step4(value3);
  7. // Do something with value4
  8. } catch (e) {
  9. // Handle any error from step1 through step4
  10. }
  11. }

正如阮一峰在博客中所述,异步编程的语法目标,就是怎样让它更像同步编程,使用async/await的方法,使得异步编程与同步编程看起来相差无几了。

六、借助流程控制库

随着Node开发的流行,NPM社区中出现了很多流程控制库可以供开发者直接使用,其中很流行的就是async库,该库提供了一些流程控制方法,注意这里所说的async并不是标题五中所述的async函数。而是第三方封装好的库。其官方文档见http://caolan.github.io/async/docs.html

async为流程控制主要提供了waterfall(瀑布式)、series(串行)、parallel(并行)

  • 如果需要执行的任务紧密结合。下一个任务需要上一个任务的结果做输入,应该使用瀑布式
  • 如果多个任务必须依次执行,而且之间没有数据交换,应该使用串行执行
  • 如果多个任务之间没有任何依赖,而且执行顺序没有要求,应该使用并行执行

    关于async控制流程的基本用法可以参考官方文档或者Async详解之一:流程控制

    下面我举一个例子说明:假设我们有个需求,返回100加1再减2再乘3最后除以4的结果,而且每个任务需要分解执行。

    1.使用回调函数
  1. function add(fn) {
  2. var num=100;
  3. var result=num+1;
  4. fn(result)
  5. }
  6. function minus(num,fn){
  7. var result=num-2;
  8. fn(result);
  9. }
  10. function multiply(num,fn){
  11. var result=num*3;
  12. fn(result);
  13. }
  14. function divide(num,fn){
  15. var result=num/4;
  16. fn(result);
  17. }
  18. add(function (value1) {
  19. minus(value1, function(value2) {
  20. multiply(value2, function(value3) {
  21. divide(value3, function(value4) {
  22. console.log(value4);
  23. });
  24. });
  25. });
  26. });

从上面的结果可以看到回调嵌套很深。

2.使用async库的流程控制

由于后面的任务依赖前面的任务执行的结果,所以这里要使用watefall方式。

  1. var async=require("async");
  2. function add(callback) {
  3. var num=100;
  4. var result=num+1;
  5. callback(null, result);
  6. }
  7. function minus(num,callback){
  8. var result=num-2;
  9. callback(null, result);
  10. }
  11. function multiply(num,callback){
  12. var result=num*3;
  13. callback(null, result);
  14. }
  15. function divide(num,callback){
  16. var result=num/4;
  17. callback(null, result);
  18. }
  19. async.waterfall([
  20. add,
  21. minus,
  22. multiply,
  23. divide
  24. ], function (err, result) {
  25. console.log(result);
  26. });

可以看到使用流程控制避免了嵌套。

七、使用Web Workers

Web Worker是HTML5新标准中新添加的一个功能,Web Worker的基本原理就是在当前javascript的主线程中,使用Worker类加载一个javascript文件来开辟一个新的线程,起到互不阻塞执行的效果,并且提供主线程和新线程之间数据交换的接口:postMessage,onmessage。其数据交互过程也类似于事件发布/监听模式,异能实现异步操作。下面的示例来自于红宝书,实现了一个数组排序功能。

页面代码:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Web Worker Example</title>
  5. </head>
  6. <body>
  7. <script>
  8. (function(){
  9. var data = [23,4,7,9,2,14,6,651,87,41,7798,24],
  10. worker = new Worker("WebWorkerExample01.js");
  11. worker.onmessage = function(event){
  12. alert(event.data);
  13. };
  14. worker.postMessage(data);
  15. })();
  16. </script>
  17. </body>
  18. </html>

Web Worker内部代码

  1. self.onmessage = function(event){
  2. var data = event.data;
  3. data.sort(function(a, b){
  4. return a - b;
  5. });
  6. self.postMessage(data);
  7. };

把比较消耗时间的操作,转交给Worker操作就不会阻塞用户界面了,遗憾的是Web Worker不能进行DOM操作。

参考文献

Javascript异步编程的4种方法-阮一峰

《You Don't Know JS:Async&Performance》

《JavaScript设计模式与开发实践》-曾探

《深入浅出NodeJS》-朴灵

《ES6标准入门-第二版》-阮一峰

《JavaScript Web 应用开发》-Nicolas Bevacqua

《JavaScript高级程序设计第3版》

JavaScript异步编程的主要解决方案—对不起,我和你不在同一个频率上的更多相关文章

  1. Func-Chain.js 另一种思路的javascript异步编程解决方案

    本文转载自:https://www.ctolib.com/panruiplay-func-chain.html Func-Chain.js 另一种思路的javascript异步编程,用于解决老式的回调 ...

  2. javascript异步编程的前世今生,从onclick到await/async

    javascript与异步编程 为了避免资源管理等复杂性的问题, javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为 ...

  3. 5分种让你了解javascript异步编程的前世今生,从onclick到await/async

      javascript与异步编程 为了避免资源管理等复杂性的问题,javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是 ...

  4. 深入解析Javascript异步编程

    这里深入探讨下Javascript的异步编程技术.(P.S. 本文较长,请准备好瓜子可乐 :D) 一. Javascript异步编程简介 至少在语言级别上,Javascript是单线程的,因此异步编程 ...

  5. JavaScript 异步编程的前世今生(上)

    前言 提到 JavaScript 异步编程,很多小伙伴都很迷茫,本人花费大约一周的业余时间来对 JS 异步做一个完整的总结,和各位同学共勉共进步! 目录 part1 基础部分 什么是异步 part2 ...

  6. 探索Javascript异步编程

    异步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript越来越广的被使用,大量的异步IO操作使得该问题变得明显.许多不同的方法都可以解决这个问题,本文讨论了一些方法 ...

  7. javascript异步编程方案汇总剖析

    code[class*="language-"] { padding: .1em; border-radius: .3em; white-space: normal; backgr ...

  8. 探索Javascript 异步编程

    在我们日常编码中,需要异步的场景很多,比如读取文件内容.获取远程数据.发送数据到服务端等.因为浏览器环境里Javascript是单线程的,所以异步编程在前端领域尤为重要. 异步的概念 所谓异步,是指当 ...

  9. javascript异步编程,promise概念

    javascript 异步编程 概述 采用单线程模式工作的原因: 避免多线dom操作同步问题,javascript的执行环境中负责执行代码的线程只有一个 内容概要 同步模式和异步模式 事件循环和消息队 ...

随机推荐

  1. web网页中使用vlc插件播放相机rtsp流视频

    可参考: 使用vlc播放器做rtsp服务器 使用vlc播放器播放rtsp视频 使用vlc进行二次开发做自己的播放器 vlc功能还是很强大的,有很多的现成的二次开发接口,不需配置太多即可轻松做客户端播放 ...

  2. Window系统性能获取帮助类

    前言: 这个是获取Windows系统的一些性能的帮助类,其中有:系统内存.硬盘.CPU.网络(个人测试还是比较准的).Ping.单个进程的内存.Cpu.网络(不准).    最初在这个的时候在各种搜索 ...

  3. HTML 语义化之b_i_em_strong

    默认效果 i和em都是斜体.b和strong都是加粗. 语义区别: em 和 strong 分别表示句中强调和全局加重强调 搜索引擎中更受重视,一些语音阅读器也会根据它在阅读时加强语气. i 和 b ...

  4. SqlDataReader和SqlDataAdapter

    SqlDataReader 高效,功能弱,只读访问SqlDataAdapter 强大,要求资源也大一点 SqlDataReader 只能在保持跟数据库连接的状态下才可以读取... SqlDataAda ...

  5. iOS小知识:计算字符串长度(如果有表情,表情的长度为1)

    在做项目的时候,textField能够输入表情,但是iOS的表情是占两个字符的,再计算字符串长度的时候就和想象的不一样了,所以用了次方法会将表情的长度转成1,最后得到的字符串的长度就是能看到的实际的长 ...

  6. 【Android接百度地图API】百度地图Demo点击按钮闪退

    运行百度地图自带的BaiduMap_AndroidSDK_v4.1.0_Sample里面的BaiduMapsApiASDemo发现点击上面的按钮会闪退,控制台报的是xml的问题 查了一下,官方文档特别 ...

  7. PHP中::、-&gt;、self、$this操作符的区别

    在访问PHP类中的成员变量或方法时,如果被引用的变量或者方法被声明成const(定义常量)或者static(声明静态),那么就必须使用操作符::,反之如果被引用的变量或者方法没有被声明成const或者 ...

  8. C和指针 第十七章 经典数据类型 堆栈 队列 二叉树

    堆栈: // // Created by mao on 16-9-16. // #ifndef UNTITLED_STACK_H #define UNTITLED_STACK_H #define TR ...

  9. DEV MessageBox

    DialogResult dr = DevExpress.XtraEditors.XtraMessageBox.Show("确定要删除所有错误映射数据吗?", "提示&q ...

  10. offsetParent的解释

    offsetParent是个只读属性,返回最近显示指定位置的容器元素的父级.如果元素没有指定位置,最近的元素或者根元素(标准模式下是html,怪异模式下是body)就是offsetParent off ...