请教一个有关 Promise 执行顺序的问题,有关 resolve 一个 promise 以及注册 callback 的顺序

2018-03-03 23:57:24 +08:00
 benyuwan
const original = Promise.resolve(2)
new Promise(resolve => {
	resolve(original)
	Promise.resolve().then(() => Promise.resolve().then(() => console.log(1)))
	console.log(4)
}).then(t => console.log(t))
console.log(3)

这样一段代码.

按照我的理解

因为 original 已经被 resolve,所以它的状态被 new Promise 继承

然后执行 Promise.resolve 并注册回调

然后打印 4

然后注册 new Promise 的回调

然后打印 3

然后执行第一个注册的回调,并注册第三个回调

然后打印 t,也就是 2

然后执行第三个回调,也就是 1

可是执行结果却是 4,3,1,2

想不通为什么,特来求教!

谢谢各位大牛~~~

2663 次点击
所在节点    问与答
7 条回复
noe132
2018-03-04 07:35:04 +08:00
根据 Promise A+ 规范 https://promisesaplus.com/#point-34

Promise 的执行顺序是和平台相关的。promise 链的执行是在当前 event loop 的主线程结束后的某一时刻进行,具体是何时得看 js 引擎的实现或者 polyfill 的实现。( 2.2.4 )

从 v8 的实现来看,Promise 的回调是存放在 microtasks 里,event loop 会先完成 microtasks 再去执行 tasks
从 polyfill 来看,得看实现的方式。如果用 mutationObserver,会和 v8 的顺序相似。如果使用 setTimeout 或者 setImmediate,顺序就会有所不同了。总之,不同 Promise 的执行顺序并不在 Promise 的规范里,顺序也是没法保障的,只不过可以对特定情况进行分析,来了解当前 JS 解释器的执行顺序。例如我用 bulebird 的结果是 4, 3, 2, 1。

我尝试分析了一下,
:3 resolve(original)这一步在
:6 .then(t => console.log(t))
时,由于 original 是 Promise,在执行 promise resolution procedure(参考规范第 2.3 节)的时候比 resolve 非 promise 的值多了一步,需要等待被 resolve 的 promise fullfilled 或 rejected。我猜想由于是懒惰执行,所以到第 6 行 then 的时候在发现 resolve 的是一个 promise,需要等待这个 promise fullfill,于是就把:6 的 then 的回调 chain 在了 original 的后面(参考 2.3.2 )

我把格式略微优化了一下,附上 microtasks queue 的操作过程,如下图


总结来看,这种顺序是不能作为逻辑依赖的,因为规范并没有这样要求,能保证的只是每一个 chain 的顺序。
我对 v8 了解不多,所以分析过程可能不严谨,也许会有些错误,仅供参考,详细了解还得参考 promise A+和 v8 的实现。
noe132
2018-03-04 07:37:29 +08:00
如果把 original 从 Promise 替换成一个非 promise 的值如 2
结果就是 4, 3, 2, 1 了,大概就是在 pop 11 的时候直接执行了,而不是再 chain 一步等待 original 去 fullfill
des
2018-03-04 07:54:03 +08:00
后排说一下,移动端是 4321
benyuwan
2018-03-04 10:30:10 +08:00
@noe132 感谢你的回答!不过我有一些疑惑的地方。。

首先我确实只在 chrome safari 和 ff 上试过。

这三个浏览器的实现是一致的。。

其次 Promise.resolve(1)应该是会立刻 fulfilled 吧?

然后 resolve(original)继承了它的状态。

关于你的解释我无法理解的一点是,一个没有 fulfilled 的 promise,它的 callback 为什么会加入到 micro queue 里?
mdluo
2018-03-04 10:43:59 +08:00
“因为 original 已经被 resolve,所以它的状态被 new Promise 继承” 这句话并不正确,很简单的验证:



所以可以看到在 Promise 的构造函数的参数即 executor 函数,在执行的时候去 resolve 另外一个 Promise,即使这个 Promise 的状态是 "resolved",也不会在构造函数返回的时候就立即把 promise 对象的状态置为 "resolved",确实是 "pending"。

但是如果 resolve(original) 和 original.then((value) => resolve(value)) 是等同的话,结果应该是 4 3 2 1 才对)。因为这个 resolve(value) 虽然不是在第一个 event loop 里同步执行的,但是是最早加入 microtask queue 的。说明 V8 在针对 executor 的 resolve 函数的调用时机的处理并不是同步的,其他有些 Promise 的实现(比如 bluebird )是同步的,结果也确实是 4 3 2 1。

V8 的处理好像可以等同于下图,看了下这样的话不管是结果还是执行过程中的 Promise 状态都是一致的



总的来说就是,executor 的 resolve 很有可能在 V8 的 Promise 实现里被特殊处理了,resolve(original) 的执行过程都不是在同步代码里,而是加入了 microtask queue。在 microtask queue 里执行的时候又因为是去 resolve 另一个 Promise,相当于 resolve 这个 Promise 的 then 结果,所以又被加入了 microtask queue 的最后面。而最终轮到 resolve(value) 执行的时候,前面已经被一个 Promise.resolve().then() 的回调、以及这个回调带来的另外一个回调给 “插队” 了,所以 resolve(value) 的执行被排在了最后。

没具体去看 V8 的代码,仅仅从表现上分析的。不过这一点确实是规范里也没有提到的东西,跟实现有关。
noe132
2018-03-04 12:43:38 +08:00
其实问题主要在于 resolve promise 对象的实现。本想看看 v8 是怎么实现的,奈何 c++实在看不懂。。。
benyuwan
2018-03-04 13:59:17 +08:00
@mdluo 你好,感谢你的回答

不知道我理解你的意思理解的对不对?

V8 对 executor 里的 resolve 一个 promise 并不是同步处理的,而是在注册 callback 的时候将这个 resolve(Promise.resolve())加入到 microtask queue 里,这样就可以解释了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/434583

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX