【回归基础(一)】JS 关于Promise属性和方法
最近在看一些前端的小视频,感觉自己对js的理解从精通到略懂(doge),故借本文重新回归基础
【回归基础(一)】JS 关于Promise属性和方法
我们都知道 JS 是单线程,但是一些操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种的执行模式:同步和异步模式。
JS 中用来存储待执行回调函数的队列包含 2 个不同特定的队列
- 宏队列: 用来保存待执行的宏任务(回调), 比如:定时器回调 / DOM 事件回调 / ajax 回调
- 微队列: 用来保存待执行的微任务(回调), 比如:promise的回调 / MutationObserver的回调
- JS执行时会区别这两个队列
- JS 引擎首先必须先执行所有的初始化同步任务代码
- 每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行
每当执行宏任务之前就得看看是否有微任务,有就先执行当前队列中的所有微任务
(虽然但是)随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法(但是网络上的教程仍然使用“宏队列”一词)。宏任务队列包含一些较大的、离散的任务,例如 I/O 操作、用户交互事件(例如点击、键盘输入)、定时器事件。用于处理离散任务,个人认为可以理解为除了微队列的其它队列。
在目前 chrome 的实现中,至少包含了下面的队列:
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 微队列:用户存放需要最快执行的任务,优先级「最高」
了解了这些我们就可以来学习Promise了
添加任务到微队列的主要方式主要是使用 Promise
添加任务到微队列的主要方式主要是使用 Promise、MutationObserver。例如:
1
2
// 立即把一个函数添加到微队列
Promise.resolve().then(函数)
Promise
是一个处理异步操作的对象,它代表了一个异步操作的最终完成或者失败的值,Promise 提供了一种在单个异步操作成功或失败时进行回调的机制,并且可以链接多个异步操作形成复杂的流程控制。
本质上 Promise 是一个函数返回的 对象 ,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。例如,我们可以把
1 |
|
优化成
1 |
|
resolve 和 reject:
resolve
是 Promise 构造函数内部传入的一个函数引用,当异步操作成功完成时调用它。调用resolve(value)
会使得 Promise 变为 fulfilled 状态,并将结果value
传递给后续通过.then()
注册的成功回调。
1 |
|
reject
同样是 Promise 内部的函数引用,当异步操作失败时调用它。调用reject(reason)
会使得 Promise 变为 rejected 状态,并将错误信息reason
传递给后续通过.catch()
或.then()
的第二个参数注册的错误回调。
1 |
|
Promise的三种状态:
- fulfilled(已履行/已解决):Promise 在其异步操作成功完成后所处的状态,此时可以通过
.then()
方法来获取到成功的结果。 - rejected(已拒绝):Promise 在其异步操作失败后所处的状态,此时可以通过
.catch()
或.then()
的第二个参数来捕获和处理错误信息。 - pending(进行中):这是Promise对象的初始状态,当一个Promise被创建但其异步操作尚未完成时,它处于pending状态。在pending状态下,Promise既没有fulfilled也没有rejected。
一、链式调用
连续执行两个或者多个异步操作是一个常见的需求,在旧的回调风格中,这种操作会导致经典的回调地狱:
1 |
|
我们就可以通过一个 Promise 链来解决这个问题。在Promise API中,回调函数是附加到返回的 Promise 对象上的,而不是传入一个函数中!
1 |
|
在上述例子中,then()
函数会返回一个和原来不同的新的 Promise
promise2
不仅表示 doSomething()
函数的完成,也代表了你传入的 successCallback
或者 failureCallback
的完成,这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,这样的话,在 promise2
上新增的回调函数会排在这个 Promise 对象的后面。
每一个 Promise 都代表了链中另一个异步过程的完成。此外,then
的参数是可选的,catch(failureCallback)
等同于 then(null, failureCallback)
——所以如果你的错误处理代码对所有步骤都是一样的,你可以把它附加到链的末尾
1 |
|
不过建议在使用箭头函数的时候写上大括号和和return(其实也看情况和个人喜好),以便支持使用多个语句,
如果上一个处理程序启动了一个 Promise 但并没有返回它,那就没有办法再追踪它的状态了,这个 Promise 就是“漂浮”的
1 |
|
一个经验法则是,每当你的操作遇到一个 Promise,就返回它,并把它的处理推迟到下一个
then
处理程序中
就像这样
1 |
|
二、嵌套
嵌套 Promise 是一种可以限制 catch
语句的作用域的控制结构写法。明确来说,嵌套的 catch
只会捕获其作用域及以下的错误,而不会捕获链中更高层的错误。如果使用正确,可以实现高精度的错误恢复。
1 |
|
这个内部的 catch
语句仅能捕获到 doSomethingOptional()
和 doSomethingExtraNice()
的失败,并将该错误与外界屏蔽,之后就恢复到 moreCriticalStuff()
继续执行。值得注意的是,如果 doSomethingCritical()
失败,这个错误仅会被最后的(外部)catch
语句捕获到,并不会被内部 catch
吞掉。即,使用一个 catch
,这对于在链式操作中抛出一个失败之后,再次进行新的操作会很有用。如果任何一个函数调用失败,就会执行catch中的失败回调函数。
1 |
|
在这个例子中,promise1
内部的.then()
方法返回了promise2
,导致了Promise的嵌套。当promise1
成功解决后,紧接着执行promise2
,并在其解决后打印结果。
为了避免过多的嵌套,可以利用Promise链式调用(Chaining)来简化代码,使其更易读和维护。通过将每个异步操作的结果传递给下一个.then()
,可以消除嵌套:
1 |
|
一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,就立即返回它,最终的链应是扁平化的:
1 |
|
上述代码的写法就是具有适当错误处理的简单明确的链式写法。使用async/await
时可以解决以上大多数错误
三、组合
有四个组合工具可用来并发异步操作:Promise.all()
、Promise.allSettled()
、Promise.any()
和 Promise.race()
。
他们是Promise对象提供的方法,它们用于处理多个Promise实例的集合。以下是每个方法的用法和作用
1. Promise.all(iterable)
- 用法:接收一个可迭代对象(通常是一个Promise实例数组)作为参数。
- 作用:只有当所有传入的Promise都变为fulfilled状态时,
Promise.all()
返回的Promise才会变为fulfilled,并且其结果是一个包含所有Promise resolve值的数组,顺序与原数组保持一致。只要其中有一个Promise变为rejected状态,那么Promise.all()
返回的Promise就会立即变为rejected,并返回第一个被reject的Promise的结果。
Javascript
1 |
|
2. Promise.allSettled(iterable)
- 用法:同样接受一个Promise实例组成的可迭代对象作为参数。
- 作用:不论传入的Promise是fulfilled还是rejected,
Promise.allSettled()
都会等待所有Promise完成(settled),然后返回一个新的Promise,该Promise在所有子Promise都settled后resolve,结果是一个数组,数组中的元素包含了每个Promise的状态(fulfilled或rejected)以及对应的值或拒绝原因。
Javascript
1 |
|
3. Promise.any(iterable)
- 用法:从ES2021开始引入,它接受一组Promise实例作为参数。
- 作用:只要输入的Promise中有任意一个变为fulfilled状态,
Promise.any()
返回的Promise就会变为fulfilled,并返回那个率先fulfilled的Promise的resolve值。如果所有Promise都变为rejected状态,那么返回的Promise也会变为rejected,其reason是一个AggregateError对象,包含了所有Promise的拒绝原因。
Javascript
1 |
|
4. Promise.race(iterable)
- 用法:接收一组Promise实例作为参数。
- 作用:一旦输入的Promise中有一个变为fulfilled或者rejected状态,
Promise.race()
返回的Promise就立即以相同的fulfilled或rejected状态结束,并返回第一个改变状态的Promise的结果或错误。
Javascript
1 |
|
四、时序问题
在基于回调的 API 中,回调函数何时以及如何被调用取决于 API 的实现者。例如,回调可能是同步调用的,也可能是异步调用的:
1 |
|
不建议使用上述这种设计,因为它会导致所谓的“Zalgo 状态”。另一方面,Promise 是一种控制反转的形式——API 的实现者不控制回调何时被调用。相反,维护回调队列并决定何时调用回调的工作被委托给了 Promise 的实现者(一般情况浏览器引擎),这样一来,API 的使用者和开发者都会自动获得强大的语义保证,包括:
- 被添加到
then()
的回调永远不会在 JavaScript 事件循环的当前运行完成之前被调用。 - 即使异步操作已经完成(成功或失败),在这之后通过
then()
添加的回调函数也会被调用。 - 通过多次调用
then()
可以添加多个回调函数,它们会按照插入顺序进行执行。
友情提示:传入
then()
的函数永远不会被同步调用,即使 Promise 已经被解决了(resolved),详情请自行了解浏览器事件循环的原理,这里放一张示意图根据 W3C 的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。
在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。- 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
Promise 回调被处理为微任务,加入微队列,而 setTimeout() 回调被处理为任务队列,加入其他线程。
五、使用async/await异步函数
异步函数(async function)是 ECMAScript 2017 (ECMA-262) 标准的规范,几乎被所有浏览器所支持。
来看下面这个实例:
1 |
|
我们可以将这段代码变得更好看:
1 |
|
异步函数 async function 中可以使用 await 指令,await 指令后必须跟着一个 Promise,异步函数会在这个 Promise 运行中暂停,直到其运行结束再继续运行异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于阅读。
处理异常的机制将用 try-catch 块实现:
1 |
|
如果 Promise 有一个正常的返回值,await 语句也会返回它:
1 |
|
六、面试题
1 |
|
参考文献:
稀土掘金 【宏队列与微队列的Promise】作者:奶香南瓜饼
[菜鸟教程 【JavaScript Promise】](JavaScript Promise | 菜鸟教程 (runoob.com))
渡一教育