JavaScript的异步编程

JavaScript有几种异步编程的解决方案。

一、回调函数

被传递给其他函数的函数叫作回调函数。回调函数把任务的第二段单独写在一个函数中,待重新执行这个任务时直接调用这个回调函数。

Node中文件操作经常有这样的应用。

使用回调函数时,如果只有一个回调,回调中不会包含其余的回调函数也还好,但是如果回调中包含回调,就会造成所谓的回调地狱,十分不利于代码的review和debug

二、事件监听

事件监听把事件的发生源和事件的发生后的操作进行了分离。

 比如ajax中对于load事件和error事件的监听,就可以使用事件监听。

三、发布与订阅

发布与订阅是对事件监听的补充,事件监听只能控制事件的操作,不能控制事件的产生。

而发布与订阅模式可以控制两方面。

在Node中的异步编程使用发布与订阅是常见的。

三、Promise对象

Promise处理少量的异步操作是没有问题的,它和回调函数的模式一样,在处理多个异步操作时,为了让代码看起来像是同步的,把所有的回调都使用了then方法进行封装,

这样使得操作本身的逻辑变的不明显,而且,Promise是不可以取消的,一旦指定了then方法的回调,再发生事件的时候就必须去执行。

之前介绍过Promise(https://www.cnblogs.com/wangtingnoblog/p/js_Promise.html),

四、Generator函数

ES6提供的异步编程的新的解决方案。

Generator函数在之前的文章简单的介绍过(https://www.cnblogs.com/wangtingnoblog/p/js_Generator.html),实际应用中Generator是实现状态机的最佳的数据结构,在异步编程中用到的比较少,

之前介绍过使用Generator函数进行异步编程(https://www.cnblogs.com/wangtingnoblog/p/js_Generator_async.html),

五、async函数

ES7提供的异步编程的终级方案,在Angular中(TypeScript)可以直接使用,其他环境下需要依靠编译器。

5.1 async的原理

  1. async函数可以理解为Generator的语法糖,这时async关键字相当于*,await相当于yield
  2. 也可以把async函数理解为Promise对象的语法糖,因为async函数返回Promise。可以把async函数看做由多个异步操作封装成的一个Promise,await命令就是内部then命令的语法糖。

我们把async理解成Generator的语法糖,来看一下它的实现: 将Generator函数和自动执行器封装在一个函数中。

async function fn(args) {// ....}相当于

function fn(args) {return spawn(function *() {// ...}) }

来看一下spawn的实现原理

 1 function spawn(genF) {
 2   return new Promise(
 3     (resolve, reject) => {
 4       // 执行生成器生成迭代器
 5       const gen = genF();
 6 
 7       // 这里把step的参数设置成函数是为了处理异常
 8       function step(nextF) {
 9         let next;
10         try {
11           // 迭代器进入下一个迭代
12           next = nextF();
13         } catch(e) {
14           // 发生异常时返回rejected的Promise
15           return reject(e);
16         }
17         // 判断是否已经迭代结束
18         if (next.done) {
19           // 把async的return的值发送出去,如果async函数没有return,则为undefined
20           resolve(next.value);
21         }
22         // 这里在外层包含Promise.resolve是为了处理await后面跟的不是Promise的情况,这也是async函数的特殊之处
23         Promise.resolve(next.value)
24         .then(
25           // 把迭代器的next方法封装成函数
26           (v) => step(function() { return gen.next(v); })
27         )
28         .catch(
29           // 发生异常时,把迭代器的throw方法封装成函数,这样在调用nextF方法时就能捕获到异常,进入reject发送出去
30           (e) => step(function() { return gen.throw(e); })
31         );
32       }
33       // 启动迭代器
34       step(function() {return gen.next(undefined);})
35     }
36   );
37 }

上面的实现和之前介绍Generator的流程管理几乎一样(spawn添加了异常处理),

async相比于Generator函数的改进体现在如下几点:

  1. 内置执行器: 把Generator的流程管理封装起来,不要开发者进行管理。
  2. 更好的语义: async表示函数中有异步操作,await表示需要等待。
  3. 更广的适用性: Generator的自动流程管理需要yield后面的表达式是函数(参数是回调函数)或者是Promise,而async的await后面则不需要,看代码的第23行。
  4. 返回值是Promise: Generator的返回值是迭代器,而async的返回值是Promise,更易使用。

TypeScript中支持async函数,我们来看一下TypeScript关于async的码源是如何写的:

 1 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
 2     // 返回一个Promise
 3     return new (P || (P = Promise))(
 4         function (resolve, reject) {
 5             // 异步操作成功之后进行下一次迭代
 6             function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
 7             // 异步操作异常之后抛出错误,在step函数中捕获,再发送出去
 8             function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
 9             // 迭代函数
10             function step(result) {
11                 // 判断迭代器是否已经到了最后
12                 result.done ?
13                     // 把return的值发送出去
14                     resolve(result.value) :
15                     // 这里相对于P.resolve(result.value)),用Peomise封装,保证await后面不是Promise也能正常执行
16                     new P(
17                         function (resolve) { resolve(result.value); }
18                     // 成功和异常处理
19                     ).then(fulfilled, rejected); }
20             step(
21                 // 启动迭代器
22                 (generator = generator.apply(thisArg, _arguments || []))
23                 .next()
24             );
25     });
26 };

发现和上面写的原理差不多,不过对异常做了更加细致的处理。

5.2 async的使用

async函数返回一个Promise对象,可以使用then方法添加回调函数。

当async函数执行时,一旦遇到await就会先返回,等到触发的异步操作完成之后,再执行函数体内后面的语句。

ajax是一个返回Promise的函数

 1   ajax(url) {
 2     return new Promise(
 3       function (resolve, reject) {
 4         const xhr = new XMLHttpRequest();
 5         xhr.open('get', url, true);
 6         xhr.responseType = 'json';
 7         xhr.setRequestHeader('Accept', 'application/json');
 8 
 9         xhr.addEventListener('load', function(ev: ProgressEvent) {
10           resolve(xhr.response);
11         });
12         xhr.addEventListener('error', function(ev: ProgressEvent) {
13           reject('发生了错误');
14         });
15         xhr.send(null);
16       }
17     );
18   }
View Code
 1 async function getFamilies(): Promise<void> {
 2     console.log('async start');
 3     const nameResponse = await this.ajax('http://localhost:3002/users?id=1');
 4     console.log('第一个await结束');
 5     const familiesResponse = await this.ajax('http://localhost:3002/family?name=' + nameResponse[0].families);
 6     console.log('第二个await结束');
 7     console.log('async end');
 8   }
 9 getFamilies();
10 console.log('这是在async函数之后的操作');

运行结果:

  • async start
  • 这是在async函数之后的操作
  • 第一个await结束
  • 第二个await结束
  • async end

可以看到使用async十分方便而且语义明确。

需要注意的是await后面的异步操作可能是rejected,所以最好把await语句放在try catch语句中。

 1   async getFamilies(): Promise<void> {
 2     console.log('async start');
 3     let nameResponse;
 4     try {
 5       nameResponse = await this.ajax('http://localhost:3001');
 6     } catch (e) {
 7       console.log(e);
 8     }
 9 
10     console.log('第一个await结束');
11     const familiesResponse =
12       await this.ajax('http://localhost:3003').catch(console.log);
13     console.log('第二个await结束');
14     console.log('async end');
15   }

有2种方式捕获异常

  1. try catch
  2. Promise 的catch

如果想要并发,可以在await后面使用Promise.all

5.3 async的说明

async函数执行到await时,执行权就会移交给调用async函数的代码,然后去执行下面的代码。直到同步的代码执行完成(栈被清空)。

在执行同步代码的过程中,可能await的异步操作已经完成,但是系统不会立即移交执行权给async函数,它也等同步的代码执行完成之后才会移交。

这是Event Loop决定的。

简单来说,开始时,栈中的函数一个一个执行,当遇到await时,就将await的后面的操作挂起,执行执行栈中的下一个函数,等到await的异步操作完成后,会把挂起的操作放在任务队列中(注意不是立即执行),

栈被清空后,系统从任务队列中提取async其余的操作继续执行。

 总结:

  关于js的异步编程主要介绍了async函数的实现方式,其他方式在其他文章中也说明过,这里就不再重复,实际应用不多,有不到位的地方欢迎各位老铁指正。

作成: 2019-02-10

修改:

  1. 2019-02-10 22:58:24
  2. 2019-02-16 19:07:34

参考:《ES6标准入门》《Learning TypeScript》《深入理解ES6》

posted on 2019-02-10 22:56  西门本不吹雪  阅读(597)  评论(0编辑  收藏  举报