谈谈我对 Promise 和异步函数的理解

开发 前端
作为项目的技术负责人,前端本不是我的主业。目前,前端团队无论从代码质量还是技术水平,都不太令人满意。我每周合并代码时,有时会瞟一眼,有些同学的代码真的堪忧。

作为项目的技术负责人,前端本不是我的主业。目前,前端团队无论从代码质量还是技术水平,都不太令人满意。我每周合并代码时,有时会瞟一眼,有些同学的代码真的堪忧。

目前,前端团队都是初级工程师,功底较差,做出来的东西只是能用而已。他们既不会总结经验,形成最佳实践;做事又随便,总是机械地应付任务。我苦口婆心地劝他们要多看看书,思考代码要怎么写更好,但收效甚微。

既然规劝没用,我就强制他们学习!我定的第一个课题是 TypeScript ,因为他们的代码质量很差,希望引入 TypeScript 后提升代码健壮性。前后端分享频率都是每周一次,但我有点忙不过来,后续改成双周一次。

组织过好几次 TypeScript 学习后,有同学提议分享自己感兴趣的研究课题,我没反对。最近李同学在研究 Promise ,因此 Promise 便成了本周的分享主题。

我是后端出身,虽然前端项目也是我在主导,但知识盲区还有很多。Promise 之前就一直没用对,真是贻笑大方。正好趁着这次机会,好好梳理下 Promise 的来龙去脉,故有此篇。

异步模型

在计算机程序中,网络和 IO 等操作通常比较耗时,为此计算机先驱们发明了两种不同的编程模型:

  • 同步 模型,操作发起后程序进入阻塞状态,等待操作完成;
  • 异步 模型,操作发起后程序先干别的事,操作完成后再通知程序处理;

Javascript 是单线程程序,为避免网络和用户操作阻塞程序,只能采用异步编程模型。异步操作发起后,Javascript 不会等待操作完成,而是接着干别的事情。

异步编程模型通常需要 回调函数( callback function )来配合,操作发起前先注册一个回调函数,操作完毕后自动执行回调函数。以在浏览器中发起原生 http 请求为例:

  1. // 操作完成后要执行的回调函数 
  2. function onLoad(event) { 
  3.     // 输出响应内容 
  4.     console.log(event.target.response) 
  5.  
  6. // 创建请求对象 
  7. const request = new XMLHttpRequest(); 
  8. // 将回调函数注册为事件处理器 
  9. // 操作完毕后将自动执行 
  10. request.addEventListener("load", onLoad); 
  11. // 发起操作 
  12. request.open("GET""https://cors.fasionchan.com/about.txt"); 
  13. request.send(); 

回调地狱

基于回调函数的编程模型并不直观:因为回调函数会层层嵌套,异步操作一多便深陷十八层地狱。

为了说明回调地狱的由来,我们凭空生造几个异步操作:

  1. // 将数据变成大写 
  2. function upperAsync(data, callback) { 
  3.     setTimeout(function () { 
  4.         callback(data.toUpperCase()); 
  5.     }, 1000); 
  6.  
  7. // 将数据变为反序 
  8. function reverseAsync(data, callback) { 
  9.     setTimeout(function () { 
  10.         callback(data.split("").reverse().join("")); 
  11.     }, 1000); 
  12.  
  13. // 将数据重复几遍 
  14. function repeatAsync(data, n, callback) { 
  15.     setTimeout(function () { 
  16.         callback(data.repeat(n)) 
  17.     }, 1000); 

这些原本都是简单的数据操作,但我们暂且将它们当成异步操作,需要一秒钟才能完成。执行这些异步操作时,除了将数据作为参数传进去,还提供一个回调函数:

  1. const data = "abc"
  2.  
  3. upperAsync(data, function (uppered) { 
  4.     console.log(`after upper: ${uppered}`); 
  5. }); 

数据处理完毕后,upperAsync 函数将调用回调函数,并将处理结果传回来。因此,一秒钟之后,我们将看到屏幕中输出:after upper: ABC。

这看起来还算直观,但如果将几个异步操作串起来,就另当别论了。如果我们想将数据先变成大写,再反序,最后重复两遍,回调函数得层层嵌套:

  1. const data = "abc"
  2.  
  3. upperAsync(data, function (uppered) { 
  4.     console.log(`after upper: ${uppered}`); 
  5.  
  6.     reverseAsync(uppered, function (reversed) { 
  7.         console.log(`after reverse: ${reversed}`); 
  8.  
  9.         repeatAsync(reversed, 2, function (repeated) { 
  10.             console.log(`after repeat: ${repeated}`); 
  11.         }); 
  12.     }); 
  13. }); 

我们先发起 upperAsync 操作,在回调函数中发起 reverseAsync ;而在 reverseAsync 的回调函数,我们还需要发起 repeatAsync 操作;repeatAsync 操作的回调函数将得到最终处理结果。

回调函数层层嵌套,代码缩进也越来越深。请看 console.log 语句,一个比一个深。这个例子才串联了三个异步操作,看着就这么费劲。如果异步操作很多,又该怎么办呢?

Promise

为了解决层层嵌套的回调地狱,前端先驱们发明了 Promise 。Promise 是一种特殊的对象,代表异步操作的执行结果(未来):既可能是成功,也可能是失败。

创建 Promise 对象,我们需要先准备一个执行器。执行器是一个函数,它接收两个参数:

  • resolve 函数,执行成功时调用,将执行结果告诉 Promise ;
  • reject 函数,执行失败时调用,将错误原因告诉 Promise ;

通过 Promise 对象的 then 方法可以注册成功时执行的回调函数;通过 catch 方法可以注册失败时的回调函数。

以 upperAsync 操作为例:

  1. const data = "abc"
  2.  
  3. const promise = new Promise(function (resolve) { 
  4.     upperAsync(data, function (uppered) { 
  5.         resolve(uppered); 
  6.     }) 
  7. }); 
  8.  
  9. promise.then(function (uppered) { 
  10.     console.log(`after upper: ${uppered}`); 
  11. }); 
  • 创建 Promise 对象,执行器执行 upperAsync 操作,结果通过 resolve 告诉 Promise ;
  • 调用 Promise 对象 then 方法注册回调函数,当 upperAsync 操作完成后执行,输出操作结果;

这个例子乍一看似乎将问题复杂化了,代码也繁琐了许多。如果将异步操作预先封装成 Promise 版本,情况就好很多。以 upperAsync 操作为例,将其封装成 Promise 版本 upperPromise :

  1. function upperPromise(data) { 
  2.     return new Promise(function (resolve) { 
  3.         upperAsync(data, function (uppered) { 
  4.             resolve(uppered); 
  5.         }); 
  6.     }); 

这样我们只需调用 upperPromise ,得到一个 Promise 对象,代表未来的执行结果。然后我们执行 Promise 对象的 then 方法,注册操作成功时的回调函数,对结果进行输出:

  1. const data = "abc"
  2.  
  3. upperPromise(data).then(function (uppered) { 
  4.     console.log(`after upper: ${uppered}`); 
  5. }); 

代码看着清爽了很多,但感觉也只是换了种写法而已?

链式调用

Promise 真正的杀手锏是 链式调用 :

  • then 方法里面可以发起另一个异步操作,返回 Promise 对象;

then 方法后面可以再次调用 then 方法,为新 Promise 对象注册回调函数;

这样一来,通过 Promise 链式调用,我们可以将多个异步操作串联起来,再也不用层层嵌套:

  1. const data = "abc"
  2.  
  3. upperPromise(data).then(function (uppered) { 
  4.     console.log(`after upper: ${uppered}`); 
  5.     return reversePromise(uppered); 
  6. }).then(function (reversed) { 
  7.     console.log(`after reverse: ${reversed}`); 
  8.     return repeatPromise(reversed, 2); 
  9. }).then(function (repeated) { 
  10.     console.log(`after repeat: ${repeated}`); 
  11. }); 
  • 执行 upperPromise 操作得到一个 Promise 对象;
  • 执行 then 方法注册回调函数,操作成功时打印结果并发起 reversePromise 操作;
  • then 回调函数返回 reversePromise 操作的 Promise ,第二个 then 为它注册回调函数;
  • 当 reversePromise 成功完成,执行第二个 then 回调,打印结果并发起 repeatPromise 操作;
  • 第三个 then 为 repeatPromise 返回的 Promise 注册回调函数,打印最终结果;

有了链式调用,可以很清晰地串联多个异步操作,而不用一层又层地嵌套回调函数。注意到,每个 console.log 语句的缩进都是一样的,它们都属于同一层。这种写法更符合人的直观感受——程序从上往下依次执行。

异步函数

后来 ES 又引入了异步函数,彻底解决了这个问题。

异步函数是指被 async 修饰的函数,在其内部可以通过 await 关键字来等待 Promise 对象的最终结果。当 Promise 对象兑现后,await 语句便可结束等待,并取得结果。

我们将上面的例子改为异步函数版本,看着舒服多了:

  1. async function process(data) { 
  2.     const uppered = await upperPromise(data); 
  3.     console.log(`after upper: ${uppered}`); 
  4.  
  5.     const reversed = await reversePromise(uppered); 
  6.     console.log(`after reverse: ${reversed}`); 
  7.  
  8.     const repeated = await repeatPromise(reversed, 2); 
  9.     console.log(`after repeat: ${repeated}`); 
  10.  
  11. const data = "abc"
  12. process(data); 

process 函数被 async 关键字装饰,因此是一个异步函数。它调用 upperPromise 操作后得到一个 Promise 对象,然后 await 等待 Promise 的兑现操作结果;最后将结果赋值到变量 uppered 。

接着,它以同样的方式依次执行 reversePromise 和 repeatPromise 操作,彻底将回调函数抛之脑后。除了增加的 async 和 await 关键字外,编程模型跟其他语言没有任何区别了。

await 除了能够等待 Promise 对象外,还能等待另一个异步函数返回结果。实际上,异步函数是针对 Promise 对象的再次封装,从语法层面对 Promise 编程模型的进一步优化。我们执行一个异步函数,得到一个 Promise 对象,这也是异步函数可以被 await 的原因。

本文算是我作为后端研发对 Javascript 异步编程模型和演化历程的初步理解,从最开始的 回调函数 以及它带来的回调地狱,到 Promise 以及它的链式调用,再到终极解决方案—— 异步函数 。发展脉络如此清晰,引人入胜。

学习一个新东西,我通常不会一开始就去啃那些抽象的定义,记忆那些繁琐的概念和用法。相反,我会从技术背景入手,看它用怎样的方式,解决了什么问题。明确技术设计思路和演进路径后,一切都变得自然而然。

 

现在算是掌握了 Promise 和异步函数的一些皮毛,后续有机会再写写李同学分享的 Promise 高级用法和实现原理,敬请期待!

 

责任编辑:武晓燕 来源: 小菜学编程
相关推荐

2023-01-12 11:23:11

Promise异步编程

2017-06-02 09:47:29

网络分层协议

2022-02-10 14:38:28

前端框架浏览器

2021-09-12 22:22:15

前端

2023-11-28 12:25:02

多线程安全

2022-06-30 09:10:33

NoSQLHBaseRedis

2022-09-19 07:57:59

云服务互联网基础设施

2022-08-14 07:14:50

Kafka零拷贝

2022-10-09 15:18:31

SwaggerOpenAPI工具

2020-10-12 10:00:11

前端react.jsjavascript

2018-11-29 08:00:20

JavaScript异步Promise

2013-07-11 10:37:20

Java内存模型

2017-05-24 10:12:54

前端FlexboxCSS3

2022-07-06 08:30:36

vuereactvdom

2022-03-21 09:05:18

volatileCPUJava

2022-08-26 00:21:44

IO模型线程

2022-09-06 11:13:16

接口PipelineHandler

2022-09-23 11:00:27

KafkaZookeeper机制

2020-08-31 07:19:57

MonoFlux Reactor

2009-08-20 18:11:08

C#异步委托
点赞
收藏

51CTO技术栈公众号