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的原理
- async函数可以理解为Generator的语法糖,这时async关键字相当于*,await相当于yield
- 也可以把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函数的改进体现在如下几点:
- 内置执行器: 把Generator的流程管理封装起来,不要开发者进行管理。
- 更好的语义: async表示函数中有异步操作,await表示需要等待。
- 更广的适用性: Generator的自动流程管理需要yield后面的表达式是函数(参数是回调函数)或者是Promise,而async的await后面则不需要,看代码的第23行。
- 返回值是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 }
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种方式捕获异常
- try catch
- Promise 的catch
如果想要并发,可以在await后面使用Promise.all
5.3 async的说明
async函数执行到await时,执行权就会移交给调用async函数的代码,然后去执行下面的代码。直到同步的代码执行完成(栈被清空)。
在执行同步代码的过程中,可能await的异步操作已经完成,但是系统不会立即移交执行权给async函数,它也等同步的代码执行完成之后才会移交。
这是Event Loop决定的。
简单来说,开始时,栈中的函数一个一个执行,当遇到await时,就将await的后面的操作挂起,这时执行栈中的下一个函数,等到await的异步操作完成后,会把挂起的操作放在任务队列中(注意不是立即执行),
栈被清空后,系统从任务队列中提取async其余的操作继续执行。
总结:
关于js的异步编程主要介绍了async函数的实现方式,其他方式在其他文章中也说明过,这里就不再重复,实际应用不多,有不到位的地方欢迎各位老铁指正。
作成: 2019-02-10
修改:
- 2019-02-10 22:58:24
参考:《ES6标准入门》《Learning TypeScript》《深入理解ES6》
原文地址:https://www.cnblogs.com/wangtingnoblog/p/js_async.html