对不起,你之前学的 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 介绍核心概念的小册,不知道大家有兴趣没?

相关推荐
王哈哈^_^1 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie2 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿2 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具3 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
清灵xmf3 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据3 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161773 小时前
防抖函数--应用场景及示例
前端·javascript
334554324 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel