Javascript的异步编程知多少?

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

[[439755]]

1写在前面

Generator执行后返回什么?

Async/await的方式比Promise和Generatir好在哪里?

2同步和异步

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

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

前面知道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中的一些方法回调

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

3Promise

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

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

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

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

待定状态的Promise对象执行的话,最后要么通过一个值完成,要么就是通过一个原因拒绝。当待定状态改成为完成或拒绝状态时,我们可以使用Promise.then的形式进行链式调用。因为最后Promise.prototype.then和Promise.prototype.catch方法返回的是一个Promise,所以它们可以继续被链式调用。

Promise是如何结局回调地狱问题的?

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

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

Promise.all

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

  1. Promise.all(iterable); 

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

  1. function read(url){ 
  2.  
  3. return new Promise((resolve,reject)=>{ 
  4.  
  5. fs.readFile(url,"utf-8",(err,data)=>{ 
  6.  
  7. if(err) return err; 
  8.  
  9. resolve(data); 
  10.  
  11. }) 
  12.  
  13. }) 
  14.  
  15.  
  16. read(A).then(data=>{ 
  17.  
  18. return read(B); 
  19.  
  20. }).then(data=>{ 
  21.  
  22. return read(C); 
  23.  
  24. }).then(data=>{ 
  25.  
  26. return read(D); 
  27.  
  28. }).catch(reason=>{ 
  29.  
  30. console.log(reason); 
  31.  
  32. }) 

我们看到上面使用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. //通过Promise.all可以实现多个异步并行执行,同一时刻获取最终解决的问题 
  9. Promise.all([read(A),read(B),read(C)]).(data=>{ 
  10.     console.log(data) 
  11. }).catch(reason=>{ 
  12.     console.log(reason); 
  13. }) 

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 返回最先执行成功的参数的执行结果

4Generator

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()); 

运行结果:

那么,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.  
  9. isString("yichuan");//true 
  10. isArray(["red","green","blue"]);//true 
  1. const readFileThunk = filename=>{  
  2.   return callback=>{  
  3.     fs.readFile(filename,callback);  
  4.   }  
  5. }  
  6.   
  7. const gen = function* (){  
  8.   const data1 = yield readFileThunk("a.txt");  
  9.   console.log(data1.toString());  
  10.   const data2 = yield readFileThunk("b.txt");  
  11.   console.log(data2.toString());  
  12. }  
  13.   
  14. const g = gen();  
  15. g.next().value((err,data1)=>{   
  16.   g.next(data1).value((err,data2)=>{  
  17.      g.next(data2);  
  18.   })  
  19. })  

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

  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.  
  3. const g = gen(); 
  4.  
  5. co(g).then(res=>{ 
  6.  
  7. console.log(res); 
  8.  
  9. }) 

5Async/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. //输出顺序依次是:testWait hello yichuan 
  14. console.log(testAwaitUse()); 

6异步编程方式小结

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

7参考文章

《Javascript核心原理精讲》

《Javascript高级程序设计》

《你不知道的Javascrtipt》

《JS 异步编程六种方案》

8写在最后

 

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

 

责任编辑:武晓燕 来源: 前端万有引力
相关推荐

2021-12-04 11:17:32

Javascript继承编程

2021-12-11 18:59:35

JavascriptJSON应用

2021-12-03 15:24:45

Javascript数据类型

2013-07-15 15:35:06

2021-12-05 08:27:56

Javascript 高阶函数前端

2021-12-07 08:01:33

Javascript 垃圾回收机制前端

2020-10-15 13:29:57

javascript

2021-12-06 07:15:48

Javascript作用域闭包

2014-05-23 10:12:20

Javascript异步编程

2015-04-22 10:50:18

JavascriptJavascript异

2017-07-13 12:12:19

前端JavaScript异步编程

2016-09-07 20:43:36

Javascript异步编程

2023-08-23 13:24:00

异步编程方法

2021-03-19 10:14:28

SpringBoot项目异步调用

2012-02-13 22:50:59

集群高可用

2022-05-08 18:02:11

tunnel隧道云原生

2021-06-02 09:01:19

JavaScript 前端异步编程

2011-11-11 15:47:22

JavaScript

2010-08-16 09:15:57

2013-12-23 14:00:31

Windows 8.2Windows 8.1
点赞
收藏

51CTO技术栈公众号