Javascript的异步编程可以说在日常的前端业务开发中举足轻重,常见的异步编程有:回调函数、事件监听、Promise、Generator、Async/await。

那么:

  • 同步编程和异步编程有啥区别呢?
  • 回调地狱有哪些方法可以解决呢?
  • Promise内部究竟有几种状态?
  • Promise是如何解决回调地狱的问题?
  • Generator执行后返回什么?
  • Async/await的方式比Promise和Generatir好在哪里?

    同步和异步

    同步:就是在执行某段代码时,在该代码没有得到返回结果前,其它代码是阻塞的无法执行,但是一旦执行完成拿到返回值后,就可以执行其它代码了。

异步:就是当某段代码执行异步过程调用发出后,这段代码不会立即得到返回结果,而是挂起在后台执行。在异步调用发出后,一般通过回调函数处理这个调用后才能拿到结果。

前面知道Javascript是单线程的,如果JS都是同步代码执行可能会造成阻塞。如果使用就不会造成阻塞,就不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。

那么JS异步编程的实现方式是如何发展的呢?

早些年为了实现JS的异步编程,一般采用回调函数的方式,如:比较典型的事件回调,但是使用回调函数来实现存在一个很常见的问题,就是回调地狱。看下面的代码像不像俄罗斯套娃。

  1. fs.readFile(a,"utf-8",(err,data)=>{
  2. fs.readFile(b,"utf-8",(err,data)=>{
  3. fs.readFile(c,"utf-8",(err,data)=>{
  4. fs.readFile(d,"utf-8",(err,data)=>{
  5. ....
  6. })
  7. })
  8. })
  9. })

常见的异步编程的场景有:

  • ajax请求的回调
  • 定时器中的回调
  • 事件回调
  • Node.js中的一些方法回调

异步回调如果层级很少,可读性和代码的维护性暂时还是可以接受的,但是当层级变多后就会陷入回调地狱。

Promise

为了解决回调地狱的问题,社区提出了Promise的解决方案,ES6又将其写入语言标准,采用Promise的实现方式在一定程度上解决了回调地狱的问题。

Promise简单理解就是一个容器,里面保存了某个未来才会结束的事件的结果。从语法而言,Promise是一个可以获取异步操作消息的对象。Promise具有三个状态:

  • 待定状态pending:初始状态,既没有被完成,也没有被拒绝
  • 已完成fulfilled:操作成功完成
  • 已拒绝rejected:操作失败

关于Promise的状态切换,如果想深入研究,可以学习『有限状态机』知识点。

待定状态的Promise对象执行的话,最后要么通过一个值完成,要么就是通过一个原因拒绝。当待定状态改成为完成或拒绝状态时,我们可以使用Promise.then的形式进行链式调用。因为最后Promise.prototype.then和Promise.prototype.catch方法返回的是一个Promise,所以它们可以继续被链式调用。
未命名文件 (19).png
Promise是如何结局回调地狱问题的?

  • 解决多层嵌套问题
  • 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性

Promise主要利用三大技术来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡

Promise.all

Promise.all(iterable)可以传递一个可迭代对象作为参数,此方法对于汇总多个Promise的结果很有用,在es6中可以将多个Promise.all异步请求并行操作。当所有结果成功返回时按照顺序返回成功,当其中一个方法失败则进入失败方法。

  1. Promise.all(iterable);

使用Promise.all解决上面的异步编程问题。

  1. function read(url){
  2. return new Promise((resolve,reject)=>{
  3. fs.readFile(url,"utf-8",(err,data)=>{
  4. if(err) return err;
  5. resolve(data);
  6. })
  7. })
  8. }
  9. read(A).then(data=>{
  10. return read(B);
  11. }).then(data=>{
  12. return read(C);
  13. }).then(data=>{
  14. return read(D);
  15. }).catch(reason=>{
  16. console.log(reason);
  17. })

我们看到上面使用Promise的使用对回调地狱的解决有所提升,但是依旧不是很好维护,对此有了新的方法。

  1. function read(url){
  2. return new Promise((resolve,reject)=>{
  3. fs.readFile(url,"utf-8",(err,data)=>{
  4. if(err) return err;
  5. resolve(data);
  6. })
  7. })
  8. }
  9. //通过Promise.all可以实现多个异步并行执行,同一时刻获取最终解决的问题
  10. Promise.all([read(A),read(B),read(C)]).(data=>{
  11. console.log(data)
  12. }).catch(reason=>{
  13. console.log(reason);
  14. })

Promise.allSettled

Promise.allSettled的语法和Promise.all类似,都是接受一个可迭代对象作为参数,返回一个新的Promise。当Promise.allSettled全部处理完毕后,我们可以拿到每个Promise的状态,而不管其是否处理成功。

  1. Promise.allSettled(iterable);

Promise.any

Promise.any也是接收一个可迭代对象作为参数,any方法返回一个Promise。只要参数Promise实例有一个变成fulfilled状态,最后any返回的实例就会变成fullfiled状态;如果所有参数Promise实例都变成rejected状态,最后any返回的实例就会变成rejected状态。

Promise.race

Promise.race接收一个可迭代对象作为参数,race方法返回一个Promise,只要参数之中有一个实例率先改变状态,则race方法的返回状态就跟着改变。

Promise方法 作用
all 参数所有返回结果都为成功才返回
allSettled 参数无论返回结果是否成功,都返回每个参数执行状态
any 参数中只要有一个成功,就返回该成功的执行结果
race 返回最先执行成功的参数的执行结果

Generator

Generator生成器是es6的新关键词,Generator是一个带星号的函数,可以配合yield关键字来暂停或执行函数。

Generator最大的特点就是可以交出函数的执行权,Generator函数可以看作是异步任务的容器,需要暂停的地方使用yield语法进行标注。

  1. function* gen(){
  2. let a = yield 111;
  3. console.log(a);
  4. let b = yield 222;
  5. console.log(b);
  6. let c = yield 333;
  7. console.log(c);
  8. let d = yield 444;
  9. console.log(d);
  10. }
  11. let t = gen();
  12. t.next(1);//第一调用next函数时,传递的参数无效,因此无法打印结果
  13. t.next(2);//2
  14. t.next(3);//3
  15. t.next(4);//4
  16. t.next(5);//5

上面代码中,调用gen()后程序会被阻塞住,不会执行任何语句;而调用g.next()后程序会继续执行,直到遇到yield关键词时执行暂停;一直执行next方法,最后返回一个对象,其存在两个属性:value和done。

yield也是es6的关键词,配合Generator执行以及暂停,yield关键词最后返回一个迭代器对象,该对象有value和done两个属性,value表示返回的值,done便是当前是否完成。

  1. function* gen(){
  2. yield 1;
  3. yield* gen2();
  4. yield 4;
  5. }
  6. function* gen2(){
  7. yield 2;
  8. yield 3;
  9. }
  10. const g = gen();
  11. console.log(g.next());
  12. console.log(g.next());
  13. console.log(g.next());
  14. console.log(g.next());

运行结果:
image.png
那么,Generator和异步编程有着什么联系呢?泽呢么才能将Generator函数按照顺序一次执行完毕呢?

thunk函数

thunk函数的基本思路就是接收一定的参数,会产生触定制化的函数,最后使用定制化的函数去完成想要实现的功能。

  1. const isType = type => {
  2. return obj => {
  3. return Object.prototype.toString.call(obj) === `[object ${type}]`;
  4. }
  5. }
  6. const isString = isType("string");
  7. const isArray = isType("Array");
  8. isString("yichuan");//true
  9. isArray(["red","green","blue"]);//true
  1. const readFileThunk = filename=>{
  2. return callback=>{
  3. fs.readFile(filename,callback);
  4. }
  5. }
  6. const gen = function* (){
  7. const data1 = yield readFileThunk("a.txt");
  8. console.log(data1.toString());
  9. const data2 = yield readFileThunk("b.txt");
  10. console.log(data2.toString());
  11. }
  12. const g = gen();
  13. g.next().value((err,data1)=>{
  14. g.next(data1).value((err,data2)=>{
  15. g.next(data2);
  16. })
  17. })

我们可以看到上面的代码还是像俄罗斯套娃,理解费劲,我们进行优化以下:

  1. function fun(get){
  2. const next = (err,data)=>{
  3. const res = gen.next(data);
  4. if(res.done) return;
  5. res.value(next);
  6. }
  7. next();
  8. }
  9. run(g);

co函数库是用于处理Generator函数的自动执行,核心原理是前面讲到的通过和thunk函数以及Promise对象进行配合,包装成一个库。

Generator函数就是一个异步操作的容器,co函数接收Generator函数作为参数,并最后返回一个Promise对象。在返回的Promise对象中,co先检查参数gen是否为Generator函数。如果是就执行函数,如果不是就直接返回,并将Promise对象的状态改为resolved。co将Generator函数的内部指针对象的next方法包装成onFulfilled函数,主要是为了能够捕获到抛出的错误。关键在于next,他会反复调用自身。

  1. const co = require("co");
  2. const g = gen();
  3. co(g).then(res=>{
  4. console.log(res);
  5. })

Async/await

JS异步编程从最开始的回调函数的方式演化到使用Promise对象,再到Generator+co函数的方式,每次都有一些改变但是都不彻底。async/await被称为JS中异步终极解决方案,既能够像Generator+co函数一样用同步方式阿里写异步代码,又能够得到底层的语法支持,无需借助任何第三方库。

async是Generator函数的语法糖,async/await的优点是代码清晰,可以处理回调的问题。

  1. function testWait(){
  2. return new Promise((resolve,reject)=>{
  3. setTimeout(()=>{
  4. console.log("testWait");
  5. resolve();
  6. },1000);
  7. })
  8. }
  9. async function testAwaitUse(){
  10. await testWait();
  11. console.log("hello");
  12. return "yichuan";
  13. }
  14. //输出顺序依次是:testWait hello yichuan
  15. console.log(testAwaitUse());

异步编程方式小结

JS异步编程方式 简单总结
回调函数 最拉胯的异步编程方式
Promise es6新增语法,解决回调地狱问题
Generator 和yield配合使用,返回的是迭代器
async/await 二者配合使用,async返回的是Promise对象,await控制执行顺序

参考文章

  • 《Javascript核心原理精讲》
  • 《Javascript高级程序设计》
  • 《你不知道的Javascrtipt》
  • 《JS 异步编程六种方案》

    写在最后

    本文主要介绍了Javascript的最重要的知识点之一,也是之后开发工作中经常要接触的概念,常用的异步编程方式有:回调函数、Promise、Generator和async/await。频繁使用回调函数会造成回调地狱,Promise的出现就是解决回调地狱的,但是Promise的链式函数也有长,对于出现了async/await的终极解决方案。