JavaScript Promise 和Async/Await一看就懂

  • Post author:
  • Post category:java


回调地狱

在书写JavaScript的时候,我经常不得不去处理一些依赖于其它任务的任务!比如说我们想要得到一个图片,对其进行压缩,应用一个滤镜,然后保存它 。

我们最先需要做的事情是得到我们想要编辑的图片。getImage函数可以处理这个问题!一旦图片被成功加载,我们可以传递那个值到一个resizeImage函数。当图片已经被成功地重新调整大小时,我们想要在applyFilter函数中为图片应用一个滤镜。在图片被压缩和添加滤镜后,我们想要保存图片并且让用户知道所有的事情都正确地完成了!

最后,我们可能得到这样的结果:

额,注意到了吗?尽管它完成了事情,但是完成的并不是很好。我们最终得到了许多嵌套的回调函数,这些回调函数依赖于前一个回调函数。这通常被称为回调地狱,由于我们最终得到了大量嵌套的回调函数,这使我们的代码阅读起来特别困难。

幸运的是,我们有Promise来帮助我们摆脱困境


Promise


Promise语法

ES6引入了Promise。在许多教程中,你可能会读到这样的内容:

Promise是一个值的占位符,这个值在未来的某个时间要么resolve要么reject

这个解释太抽象,我们来细化一下

我们可以使用一个接收一个回调函数的Promise构造器创建一个promise。

控制台输入下面的代码看下返回的结果

new Promise(() => {})



结果如上图,可以看到,Promise是一个对象,它包含一个状态[[PromiseStatus]]和一个值[[PromiseValue]]。在上面的例子中,你可以看到[[PromiseStatus]]的值是pending,promise的值是undefined

不要担心 – 你将永远不会与这个对象进行交互,你甚至不能访问[[PromiseStatus]]和[[PromiseValue]]这俩个属性!但是在使用Promise的时候,这两个属性值是非常重要的。


PromiseStatus

PromiseStatus的值,也就是Promise的状态,可以是以下三个值之一:

  • ✅ fulfilled: promise已经被resolved。一切都很好,在promise内部没有错误发生。

  • ❌rejected: promise已经被rejected。哎呦,某些事情出错了。

  • ⏳pending: promise暂时还没有被解决也没有被拒绝,仍然处于pending状态

但是什么时候promise的状态是pending、fulfilled或rejected呢? 为什么这个状态很重要呢?

在上面的例子中,我们只是为Promise构造器传递了一个简单的回调函数()=> {}。然而,这个回调函数实际上接受俩个参数。第一个参数的值经常被叫做resolve或res,它是一个函数, 在Promise应该解决(resolve)的时候会被调用。第二个参数的值经常被叫做reject或rej,它也是一个函数,在Promise出现一些错误应该被拒绝(reject)的时候被调用。

上面的例子再改造一下:

new Promise((res, rej) => res("Yay!"))
new Promise((res, rej) => rej("Aww no!"))

说一个有意思的现象,chrome Promise resolved的状态不是fulfilled,而是 resolved(上图是chrome下的截图),实际上这是Chrome的一个bug,目前已经在Canary中修复了,下面上一张firefox下的截图


PromiseValue

promise的值,即[[PromiseValue]]的值,是我们作为参数传递给resolve或reject方法的值

现在我们知道如何更好控制Promise对象。但是他被用来做什么呢?


有什么用


创建和取值

最开始的时候,我们展示了一个获得图片、压缩图片、为图片应用过滤器并保存它的例子!最终,这变成了一个混乱的嵌套回调。

现在,让我们用Promise重写整个代码块

function getImage(file) {
  return new Promise((res, rej) => {
    try {
      const data = readFile(file)
      res(data) // 如果图片加载完成且一切正常,用加载完的图片解决(resolve)promise
    } catch(err) {
      rej(new Error(err)) // 果在加载文件时某个地方有一个错误,我们将会用发生的错误拒绝(reject)promise
    }
  })
}

有内置的方法来得到promise的值

对于一个promise, 我们可以使用它上面的3个方法:

  • .then(): 在一个promise被resolved后调用

  • .catch(): 在一个promise被rejected后被调用

  • .finally(): 不论promise是被resolved还是reject总是调用

getImage(file)
  .then(image => console.log(image))
  .catch(error => console.log(error))
  .finally(() => console.log("All done!"))

在getImage的例子中,为了运行它们,我们最终不得不嵌套多个回调。现在,让我们用.then

.then它自己的执行结果是一个promise。这意味着我们可以链接任意数量的.then:前一个then回调的结果将会作为参数传递给下一个then回调!在getImage的例子中,为了运行它们,我们最终不得不嵌套多个回调。现在,让我们用.then

getImage(file)
  .then(image => console.log(image))
  .then(compressedImage => applyFilter(compressedImage)) // 添加滤镜
  .then(filteredImage => saveImage(filteredImage))// 保存图片
  .catch(error => console.log(error))
  .finally(() => console.log("All done!"))

是不是比之前的嵌套回调好多了


宏任务和微任务

还是先上代码,下面的代码控制台输出顺序是什么?

console.log('Start!')
 
Promise.resolve('Promise!').then(res => console.log(res))
 
console.log('End!')


尽管JavaScript是单线程的,我们可以使用Promise添加异步任务!

下面插一段知识

事件循环

JavaScript是单线程(single-threaded): 同时只能做一个项目,在任务期间,任何其它事情发生(JavaScript默认运行在浏览器的主线程上。幸运的是,浏览器给我们一些JavaScript引擎它自己没有提供的特性:WeB API。它包括DOM API、setTimeout、HTTP请求等内容。这能帮助我们创建一些异步、非阻塞的行为。

当我们调用一个函数的时候,它会被添加到一个叫做调用栈的东西中。调用栈不是浏览器特有的,而是JS引擎的一部分。它是栈意味着它是先进后出(想想一堆煎饼)。当函数返回一个值的时候,它会被弹出栈。

可视化效果请前往链接

https://blog.csdn.net/qq_36174666/article/details/106347322

回到我们本篇文章中

在事件循环内部,实际上有2种类型的队列:宏任务(macro)队列(或者只是叫做任务队列)和微任务队列。(宏)任务队列用于宏任务,微任务队列用于微任务。

那么什么是宏任务,什么是微任务呢?

我们看到Promise在微任务列表中!当一个Promise解决(resolve)并且调用它的then()、catch()或finally()方法的时候,这些方法里的回调函数被添加到微任务队列!这意味着then(),chatch()或finally()方法内的回调函数不是立即被执行,本质上是为我们的JavaScript代码添加了一些异步行为!

那么什么时候执行then(),catch(),或finally()内的回调呢?

事件循环给与任务的优先级:

1. 当前在调用栈(call stack)内的所有函数会被执行,当它们返回值的时候,会被从栈内弹出

2. 当调用栈是空的时,所有排队的微任务会一个接一个从微任务任务队列中弹出进入调用栈中,然后在调用栈中被执行!(微任务自己也能返回一个新的微任务,有效地创建无限的微任务循环 )

3. 如果调用栈和微任务队列都是空的,事件循环会检查宏任务队列里是否还有任务。如果宏任务中还有任务,会从宏任务队列中弹出进入调用栈,被执行后会从调用栈中弹出!

举个栗子:

Task1: 立即被添加到调用栈中的函数,比如在我们的代码中立即调用它。

Task2,Task3,Task4: 微任务,比如promise中then方法里的回调

ask5,Task6: 宏任务,比如setTimeout或者setImmediate里的回调

首先,Task1返回一个值并且从调用栈中弹出。然后,JavaScript引擎检查微任务队列中排队的任务。一旦微任务中所有的任务被放入调用栈并且最终被弹出,JavaScript引擎会检查宏任务队列中的任务,将他们弹入调用栈中并且在它们返回值的时候把它们弹出调用栈

附上代码,可以自行测试

console.log('Start!')
 
setTimeout(() => {
  console.log('Timeout!')
}, 0)
 
Promise.resolve('Promise!').then(res => console.log(res))
 
console.log('End!')

Async/Await

ES7引入了一个新的在JavaScript中添加异步行为的方式并且使promise用起来更加简单!随着async和await关键字的引入,我们能够创建一个隐式的返回一个promise的async函数。但是,我们该怎么做呢?

之前,我们看到不管是通过输入new Promise(() => {}),Promise.resolve或Promise.reject,我们都可以显式的使用Promise对象创建promise。

我们现在能够创建隐式地返回一个对象的异步函数,而不是显式地使用Promise对象!这意味着我们不再需要写任何Promise对象了。

下面这两段代码是等价的

Promise.resolve('Hello!')
 
async function great(){
  return 'Hello!'
}

尽管async函数隐式的返回promise是一个非常棒的事实,但是在使用await关键字的时候才能看到async函数的真正力量

当我们等待await后的值返回一个resolved的promise时,通过await关键字,我们可以暂停异步函数。如果我们想要得到这个resolved的promise的值,就像我们之前用then回调那样,我们可以为被await的promise的值赋值为变量!

这个暂停(把异步变为同步)是什么意思呢?

看看下面这段代码的输出顺序

const one = () => Promise.resolve('One!')
 
async function myFunc(){
  const res = await one()
  console.log(res)
}
 
console.log('Before function!')
myFunc()
console.log('After function!')

因为遇到了await关键字,异步函数myFunc被暂停,

JavaScript引擎跳出异步函数,并且在异步函数被调用的执行上下文中继续执行代码(而不是阻塞)

:在这个例子中是全局执行上下文!

最终,没有更多的任务在全局执行上下文中运行!事件循环检查看看是否有任何的微任务在排队:是的,有!在解决了one的值以后,异步函数myFunc开始排队。myFunc被弹入调用栈中,在它之前中断的地方继续运行。

变量res最终获得了它的值,也就是one返回的promise被解决的值!我们用res的值(在这个例子中是字符串One!)调用console.log。One!被打印到控制台并且console.log从调用栈弹出。



版权声明:本文为qq_36174666原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。