对不起,你之前学的 promise 可能是错的!(从 ecma 标准看 promise)

你真的知道 promise 是如何运行的吗?很多手写 promise 的筒子们,你们看看这些手写后的代码是否解释下面的案例,悄悄告诉你,很可能不能哟!

先来一道开胃菜,以下内容输出什么?

javascript 复制代码
new Promise(resolve => {
    let resolvedPromise = Promise.resolve()
    resolve(resolvedPromise)
}).then(() => {
    console.log('resolvePromise resolved')
})

Promise.resolve()
   .then(() => { console.log('promise1') })
   .then(() => { console.log('promise2') })
   .then(() => { console.log('promise3') })

答案:

javascript 复制代码
promise1
promise2
resolvePromise resolved
promise3

为什么 "resolvePromise resolved" 会在 "promise2" 后输出呢?

好吧,可能是你刚才眼花了,没发挥好, 再来一道试试:

javascript 复制代码
Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})

答案:0,1,2,3,4,5,6

是不是有点感觉了。。。。。懵懵哒的。。感觉!

本文最终结论是结合了 Standard ECMA-262 2024版 对 promise 的要求,所以无需怀疑正确性。

从头讲起

为了彻底解决这些问题,我们必须具备 promise 的基础知识!

new Promise的时候,传入的回调函数会有 resolve 和 reject 参数被传入:

javascript 复制代码
new Promise(function(resolve, reject) {
   resolve(5);
});
  • Promise 的初始状态是 pending
  • 然后内部会有一个叫做 executor 的函数会自动调用(不了解 executor 没关系,不影响理解它的机制)
  • 其执行的成功和失败会让 Promise 的 state, 也就是状态发生变化,state 会在 resolve 调用的时候, "fulfilled",完成态 ,在 reject 调用的时候变为 "rejected",失败状态。

在这里,我先消除一些误解,在调用 resolve 的时候,resolve 是一个会根据参数的不同,进行差别极大的后续处理

  • 如果 resolve 的值是一个 普通值,如下图,这在ecma 标准里叫做 is not an Object,然后会执行

FullfillPromise方法,传入参数为

  • promise,当前的 promise 实例
  • resolution,传递给 resolve 的参数

FullfillPromise的执行,我简单描述一下,在 ecamscript 文档里也有,然后 promise 会变为完成态,然后遍历then 方法(所有调用的 then 方法会放到一个队列里,依次被 for 循环调用)。

这里会涉及到非常复杂的 resolve 判断,我依次过一下。

首先,第 8 条之前的规则对我们来说百分之 99.9 不会遇到,就不谈了。

从第 8 条开始,第一个我们知道,只要不传入 Object 就立马变为完成态,例如

javascript 复制代码
new Promise(function(resolve, reject) {
   resolve('hello');
});

// Promise {<fulfilled>: 'hello'}

第 9 条:
Let then be Get(resolution, "then"). If then is an abrupt completion, then return RejectPromise(promise, then.[[Value]]).

如果 resolution 也就是 resolve 的参数是一个对象,并且有 then 属性,如果这个 then 抛出异常,那么会返回 reject 的 promise

javascript 复制代码
const value = {};
Object.defineProperty(
  value,
  'then',
  { get() { throw new Error('No "then"!'); } }
);

Promise.resolve(value).catch(
  e => console.log(`Error: ${e}`)
);

// log: Error: No "then"!

Promise resolve 参数是一个对象,并且 then 属性的值不是函数,则返回这个对象本身

Let thenAction be then.[[Value]]. If IsCallable(thenAction) is false, then return FulfillPromise(promise, resolution).

javascript 复制代码
Promise.resolve(
  { then: 42 }
).then(
  value => console.log(`Resolution with: ${JSON.stringify(value)}`)
);

// log: Resolution with: { "then": 42 }

Promise resolve 参数是一个对象,并且 then 属性的值是函数,则这个函数会被当做正常传入 promise (带有 resolve 和 reject )的函数调用

javascript 复制代码
Promise.resolve(
  { then: (...args) => console.log(args) }
).then(value => console.log(`Resolution with: ${value}`));

// log: [fn, fn]
//        |   \--- reject
//     resolve

// !!! 没有触发后面的 then,因为 promise 没有 resolve

好了,有了这个基础之后,我们看下 Promise.resolve() 这个 api 的用法

Promise.resolve() 返回一个完成后的 Promise. 也就是如果给 resolve 传入的是一个 promise ,那么返回 promise,否则返回一个 fulfilled 状态 的promise。

  • Promise.resolve(5) -> 返回 fulfilled 状态的 promise
  • Promise.resolve(Promise.resolve(5)) -> 返回 fulfilled 状态的 promise

注,这里 Promise.resolve,只是暂且可以理解为传入的值会返回 fulfilled 状态的 promise ,但实际上有些边界条件很棘手,不过平时是完全用不到的(有兴趣的同学可以去研究,比如 Promise.resolve(() => Promise.resolve(5)))。

好了,奇怪的事情马上就要发生了, 先来一个铺垫!

javascript 复制代码
const promise = new Promise(function(resolve, reject) {
   resolve(5);
});
console.log(promise);

上面你猜返回什么,没错,也是 fulfilled 状态的 promise,这个很符合我们的直觉,我们接着看!

下面代码返回什么呢?

javascript 复制代码
const promise = new Promise(function(resolve, reject) {
   resolve(Promise.resolve(5));
});

console.log(promise);

你是不是同样直觉上认为是 fulfilled 状态的 promise 呢?

对不起,错了哦,是 pending 状态的 promise。

我们把上面的问题简单画成图,让大家好好看下奇怪之处!


为什么是这样呢?

直觉看来不可靠啊,我不得不去 ecmascript 262 的英文文档里面寻求答案了,其实执行 promise 有很多过程,但我们关心的是影响我们刚才结果的规则到底在哪,全部规则网址如下,有兴趣的同学自己可以去探索,跟我们之前问题相关的规则是 第13 - 15 条规则,

javascript 复制代码
13. Let thenJobCallback be HostMakeJobCallback(thenAction).
14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback).
15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
16. Return undefined.

是不是也有点懵逼,看不懂没关系,马上来翻译一下:

其中 NewPromiseResolveThenableJob(promise, resolution, thenJobCallback),NewPromiseResolveThenableJob 是抽象的一个概念,由宿主环境自定义,其中参数:

  • promise 是指当前执行的 promise
  • resolution 是指 resolve()中的参数,也就是 Promise.resolve(5)
  • thenJobCallback 是指 Promise.resolve(5) 返回的 then 方法

然后 NewPromiseResolveThenableJob 的返回值 job, 被放到微任务队列里(HostEnqueuePromiseJob(job))

所以说 resolve(Promise.resolve(5)); 中 Promise.resolve(5) 并没有执行,而是 NewPromiseResolveThenableJob 执行返回的微任务放到了 微任务队里。

好了,我回顾一下文章开始的面试题:

javascript 复制代码
new Promise(resolve => {
    let resolvedPromise = Promise.resolve()
    resolve(resolvedPromise)
}).then(() => {
    console.log('resolvePromise resolved')
})

上面的 resolve(resolvedPromise) 这就是所谓在我们眼里多了的一个微任务,那么为什么后面还有一个多了的微任务,我们接着看

javascript 复制代码
.then(() => {
    console.log('resolvePromise resolved')
})

这里调用的就是 NewPromiseResolveThenableJob 中生成的 job,如下图:

实际上调用的就是红框部分的代码,我来翻译一下:

HostCallJobCallback(then, thenable, resolvingFunctions[[Resolve]], resolvingFunctions[[Resolve]])

参数,就是我们之前的提到的:

  • 第一个参数 then ,实际上就是题里我们说的 Promise.resolve() 的 then 方法
  • 第而个参数 thenable,就是当执行的 promise 实例(new Promise)
  • 第三个参数就是 promise 实例被传入的 resolve 方法
  • 第四个参数就是 promise 实例被传入的 reject 方法

HostCallJobCallback 最终是如何调用的呢,我们接着看:

翻译成 js 代码就是(上图的 Call,就是我们 js 里函数的 call 方法)

javascript 复制代码
 Promise.resolve().then(
   promise 实例,
   promise 实例被传入的 resolve 方法, 
   promise 实例被传入的 reject 方法)

所以这里的 then 会创造一个微任务。

我们回到文章开头的两个题,我们来梳理一下:

javascript 复制代码
new Promise(resolve => {
    let resolvedPromise = Promise.resolve()
    resolve(resolvedPromise)
}).then(() => {
    console.log('resolvePromise resolved')
})

Promise.resolve()
   .then(() => { console.log('promise1') })
   .then(() => { console.log('promise2') })
   .then(() => { console.log('promise3') })

首先 resolve(resolvedPromise) 因为 resolvedPromise 不是 Object,并且包含 then 方法,那么会产生一个微任务,把 Promise.resolve().then() 放入这个方法里,然后放入微任务

接着

javascript 复制代码
Promise.resolve()
   .then(() => { console.log('promise1') })
   .then(() => { console.log('promise2') })
   .then(() => { console.log('promise3') })

把上面第一个回调 () => { console.log('promise1') } 放入微任务,微任务队列如下:

  • 第一个 () => { Promise.resolve().then() }
  • 第二个 () => { console.log('promise1') }

然后,Promise.resolve().then() 开始执行,因为有 then,产生第二个微任务,此时的微任务队列如下:

  • () => { console.log('promise1') }
  • Promise.resolve() 中 then 方法的回调函数

然后打印 promise1,接着,放入

javascript 复制代码
Promise.resolve()
   .then(() => { console.log('promise1') })
   .then(() => { console.log('promise2') })
   .then(() => { console.log('promise3') })

中第二个 then 的回调函数到微任务队列,此时的微任务队列如下:

  • Promise.resolve() 中 then 方法的回调函数
  • () => { console.log('promise2') }

接着 Promise.resolve() 中 then 方法的回调函数返回一个 promise,此时把

javascript 复制代码
new Promise(resolve => {
    let resolvedPromise = Promise.resolve()
    resolve(resolvedPromise)
}).then(() => {
    console.log('resolvePromise resolved')
})

中的 () => { console.log('resolvePromise resolved') } 放入微任务队列,此时的微任务队列如下:

  • () => { console.log('promise2') }
  • () => { console.log('resolvePromise resolved') }

后面的就很简单了,我就不赘述了,文章开头的第二个题,你来根据我上面的解释,自己尝试解释一下,是不是豁然开朗了?

这是我 Node.js 系列文章的第三篇,也是探索 Javascript 异步任务管理的第一篇,其它几篇链接如下,欢迎点赞,收藏。其实可以出一个 Javascript 介绍核心概念的小册,不知道大家有兴趣没?

相关推荐
吃杠碰小鸡3 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone3 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
Serene_Dream3 小时前
JVM 并发 GC - 三色标记
jvm·面试
xjt_09013 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农3 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king4 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳4 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵5 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星5 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_5 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js