前端进阶 - Promise原理&宏微任务
发布于 3 年前 作者 chaochen 2528 次浏览 来自 分享

读完这篇文章,你的收获有:

  1. Promise简史
  2. Promise的关键概念
  3. 可以手写符合标准的Promise
  4. 可以解答任意宏任务/微任务的题目

0. 前言

为什么写这篇文章?

JavaScript是异步语言,因此Promise的重要性不言而喻。

而我看了一些文章,觉得质量参差不齐。

于是就系统地整理了些资料,然后输出一篇文章,即帮助他人,也能让大家给我挑问题,避免自己错而不知。

由于能力有限,文中可能存在错误,望广大网友指正。

1. Promise简史

Promise并不是一个新鲜的概念,早在2011年就出现在社区里了,目的是为了解决著名的回调地狱问题。

这个概念是在JQuery Deferred Objects出现之后,开始流行的。并于2012年,Promise被提出作为规范:Promise/A+

在成为ES6标准之前,社区里也出现了许多符合Promise标准的库,如bluebird、q、when等等。

2. Promise的关键概念

“The Promise object is used for deferred and asynchronous computations. A Promise represents an operation that hasn’t completed yet, but is expected in the future.” — MDN Promise Reference

Promise的基础认知,推荐看阮一峰的《ES6 入门教程》

本文的重点是讲解一些手写Promise需要关注的关键概念。

2.1 Promise有三个状态:

  • pending
  • resolved
  • rejected

只能从pending到resolved或rejected,之后状态就凝固了。

当状态流转成resolved时,需要选择一个值作为当前Promise的value:

  • new Promise时,则是通过resolve(val)
  • promise.then时,则是通过return(需要注意的是,没有显式return时是默认return undefined

这个值可以是任意的合法JavaScript值(包括undefinedthenable对象或者promise

thenable对象是一个定义了then方法的对象或者函数

状态流转成rejected时,则需要用一个reason来作为当前Promise被reject的理由,和resolved时同理。

2.2 Promise.prototype.then

promise.then(onFulfilled, onRejected)
  • Promise/A+ 是Promise的标准规范,其中指出Promise实例只需要实现then一个方法
  • then接收两个参数,而两个参数都是可选的,意味着可以什么都不传
  • then是可以调用多次的。会按顺序调用,并且每次得到的promise状态和值都是相同的
  • 每次调用then均返回一个全新的Promise实例,这样就可以链式调用
  • then会在当前宏任务下形成一个微任务(具体介绍看下面)

2.2.1 promise的状态

then其实和Promise的构造函数是类似的,返回值都是一个新的Promise实例。

它们之前的差异在于,通过构造函数生成的promise的状态,由构造函数自身决定:

new Promise((resolve, reject) => {
	resolve(1) // 将当前的状态流转成resolved
})

而then返回的promise的状态判断需要分两步走:

  1. then的回调函数能否处理上一个promise的状态,否则直接复用上一个promise的状态
  2. 若满足条件1,则看当前回调函数能否正常处理

说得有点绕口,看下面的实例代码即可理解:

理解条件1:

let p1 = new Promise((resolve, reject) => { // Promise {<rejected>: "error1"}
	reject('error1')
})

let p2 = p1.then(console.log) // Promise {<rejected>: "error1"}

由于p1的状态是Rejected的,而p2没有传入onRejected的回调函数,因此p2的状态完全复用p1的状态。

理解条件2:

let p1 = new Promise((resolve, reject) => { // Promise {<fulfilled>: 1}
	resolve(1)
})

let p2 = p1.then(val => { // Promise {<rejected>: ReferenceError: x is not defined}
	console.log('p1 was resolved:', val)
	return x; // Uncaught referenceError
})

let p3 = p2.then(undefined, reason => 1) // Promise {<fulfilled>: 1}

p1的状态是fulfilled的,而p2onFulfilled的回调函数,但是没有正确处理,抛异常了。因此p2的状态变成了rejected,其中的reason为则报错的原因。

而此时p3刚好有onRejected的函数,也能正确处理,最后的返回值则是自己的value,因此p3的状态是fulfilled的。

2.2.2 promise的返回值

前文也提到,promise的返回值可以是任意合法的JavaScript值,包括了promise,这里重点讲下。

由于promise的返回值决定了当前promise的value,而value是其他的promise时,则说明value是未知的,依赖其他的promise的状态。

同样看看例子:

let p1 = new Promise(resolve => {
	setTimeout(resolve, 1000, 1)
}) 

let p2 = new Promise(() => p1)

p1是一个简单的定时器promise,在1000ms之后,状态会变成&lt;fulfilled: 1&gt;

p2的返回值是p1,因此p2在1000ms之内也是&lt;pending&gt;,同样会在1000ms之后,变成&lt;fulfilled: 1&gt;

2.3 Promise.prototype.catch

虽然catch不是Promise/A+标准的方法,但是也需要提一下,因为这也是常用的方法之一。

其实,catch可以理解成then的一种封装:

promise.catch(function onRejected() {}) == promise.then(undefined, function onRejected() {})

2.4 微任务 microtask

当前promise的状态变更之后,不是立即执行then方法的。此时引入了 微任务(microtask) 的概念。

与之对应的则是 宏任务(macrotask),基本的JavaScript代码则是在一个宏任务里执行的。

也可以通过其他的方式生成宏任务:setTimeoutsetInterval;而微任务则可以通过promise.thenObject.observe(已废弃)MutationObserver生成。

宏任务和微任务的关系则是这样的(此处引入winter老师在《重新前端》画的图):

即一个宏任务下,是可以有多个微任务的。

由于微任务的机制是引擎提供的,因此手写Promise的时候,可以用setTimeout来代替。

2.4.1 解析任务

分析代码的时候,可以这样分几步走:

  1. 理想情况下,如果没有任何setTimeoutpromise.then的话,则全部在一个宏任务里执行
  2. 若出现promise.then,则在当前宏任务生成一个微任务,用于执行promise.then
  3. 若出现了setTimeout,则添加一个宏任务,重复条件1

分析几个例子考验一下:

例子1:

setTimeout(console.log, 0, 0)

new Promise((resolve) => {
    console.log(1)
    resolve(2)
}).then(console.log)

console.log(3)

正确的输出顺序:
1、3、2、0

例子2:

console.log(8)

setTimeout(function() {
    console.log(0)
    Promise.resolve(4).then(console.log)
}) // 省略参数,delay默认为0

new Promise((resolve) => {
    console.log(1)
    resolve(2)
}).then(console.log)

console.log(3)

setTimeout(console.log, 0, 5)

正确的输出顺序:
8、1、3、2、0、4、5

其实,还有async/await相关的题目,如果阅读足够多的话,我再完善吧。

3. 手写Promise

其实,看到这里说明你已经掌握了几乎全部关键概念了。剩下的任务就是将这些逻辑翻译成代码。

我在github写了一份,代码逻辑都算挺清晰的,大家可以去看看。

我建议大家在写之前,再仔细看一下Promise/A+的标准规范,可以结合我的代码一起看。

清晰理解细节之后,再动手写一遍。

如果觉得不错的话,记得给我点赞 + star

撒花,感谢阅读!

回到顶部