Nodejs 的 Promise 实现是不是有问题?

2015-07-25 12:00:55 +08:00
 haozhang

我看zakas的understandinges6上面的例子实际运行出来的结果跟zakas讲的结果不一样,比如下面这个例子:

"use strict";

var p1 = new Promise((resolve, reject) => {
    resolve("this is p1");
});

var p2 = Promise.reject(2);

var p3 = new Promise((resolve, reject) => {
    resolve("this is p3");
});


var p4 = Promise.race([p1, p2, p3]);

p4
    .then(
        (value) => {
            console.log(value);
        },
        (error) => {
            console.log(error);
        }
    );

结果是:

this is p1

也就是说p4的fulfillment handler被调用了。

按照zakas讲的(我觉得zakas讲的没错啊),应该是p2初始就是rejected状态的,所以导致p4立即变成rejected,然后调用p4的rejection handler.

但现在却是p1抢先一步变成了fulfilled,然后导致p4变成fulfilled了。

4332 次点击
所在节点    程序员
29 条回复
df4VW
2015-07-25 12:08:22 +08:00
The Promise.race(iterable) method returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.

[p1, p2, p3].next() => p1
haozhang
2015-07-25 12:10:32 +08:00
@df4VW you can change [p1, p2, p3] into [p2, p1, p3] , and the result is puzzling.
yoa1q7y
2015-07-25 12:10:32 +08:00
可以看下这篇文章,仔细研究一下
http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/
oott123
2015-07-25 12:13:12 +08:00
p1 在创建的时候就被执行了,并不一定等到你 race 才开始。
haozhang
2015-07-25 12:13:53 +08:00
@oott123 明显zakas不认同你这句话。
df4VW
2015-07-25 12:16:06 +08:00
@haozhang [p2, p1, p3] => 2
haozhang
2015-07-25 12:24:05 +08:00
@df4VW 数组换个位置结果就变了...不觉得很奇怪嘛...
oott123
2015-07-25 12:24:44 +08:00
haozhang
2015-07-25 12:57:12 +08:00
@oott123 测试结果是这样的...但是zakas讲的不是这样的...zakas认为p2改变了p4,而不是p1改变了p4.
df4VW
2015-07-25 13:05:53 +08:00
@haozhang 因为是根据数组遍历的顺序的啊。。不奇怪啊,你看我一楼说的啊
haozhang
2015-07-25 13:53:29 +08:00
@df4VW 你说的是one of the promises in the iterable resolves or rejects , 哪里说按照数组的顺序来了。
otakustay
2015-07-25 14:22:12 +08:00
从我现在为止对Promise的认识来说,Zakas说的是错的,只能简单地表达下

Promise.reject的流程大概是这样的:

1. Let C be the this value.
2. Let promiseCapability be NewPromiseCapability(C).
3. ReturnIfAbrupt(promiseCapability).
4. Let rejectResult be the result of calling the [[Call]] internal method of promiseCapability.[[Reject]] with undefined as thisArgument and (r) as argumentsList.
5. ReturnIfAbrupt(rejectResult).
6. Return promiseCapability.[[Promise]].

1-3都是同步的操作,无非就是建一个叫PromiseCapability的东西出来,重点在第4步
第4步说的是调用PromiseCapability上一个叫[[Reject]]的方法,这个方法是什么,可以从规范里找出来。规范写得也是略复杂,总之找来找去,这个方法对应的是PromiseCapability Records这个东西上的[[Reject]]属性,它的解释是“The function that is used to reject the given promise object.”
然后继续往下找,可以在《Promise Reject Functions》这一节找到这个方法的说明:


1. Assert: F has a [[Promise]] internal slot whose value is an Object.
2. Let promise be the value of F's [[Promise]] internal slot.
3. Let alreadyResolved be the value of F's [[AlreadyResolved]] internal slot.
4. If alreadyResolved.[[value]] is true, then return undefined.
5. Set alreadyResolved.[[value]] to true.
6. Return RejectPromise(promise, reason).

上面这6步的重点显然在第6点,RejectPromise这个动作的说明是:

1. Assert: the value of promise's [[PromiseState]] internal slot is "pending".
2. Let reactions be the value of promise's [[PromiseRejectReactions]] internal slot.
3. Set the value of promise's [[PromiseResult]] internal slot to reason.
4. Set the value of promise's [[PromiseFulfillReactions]] internal slot to undefined.
5. Set the value of promise's [[PromiseRejectReactions]] internal slot to undefined.
6. Set the value of promise's [[PromiseState]] internal slot to "rejected".
7. Return TriggerPromiseReactions(reactions, reason).

重点在第7步,TriggerPromiseReactions:

1. Repeat for each reaction in reactions, in original insertion order
1.1. Perform EnqueueTask("PromiseTasks", PromiseReactionTask, (reaction, argument)).
2. Return undefined.

这里看到EnqueueTask就不用往下看了,这个操作是异步的,往任务队列里放一个任务来调用then/catch挂上来的回调

上面这个过程,从RejectPromise开始和new Promise(executor)是一模一样的,所以说白了new Promise和Promise.reject其实一样,到TriggerPromiseReactions为止挂上异步任务

所以new Promise(executor)里直接resolve和Promise.resolve是一个概念,都是“当前Promise的状态变了,所有回调加到任务队列”,那后面到底哪个会影响p4就完全看遍历顺序了
haozhang
2015-07-25 15:44:52 +08:00
@otakustay 我写了以下的代码
```javascript
"use strict";

var p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("this is p1");
}, 2000);
});

var p2 = Promise.reject(2);

var p3 = new Promise((resolve, reject) => {
resolve("this is p3");
});


var p4 = Promise.race([p1, p2, p3]);

p4
.then(
(value) => {
console.log("p4 fulfilled", value);
},
(error) => {
console.log("p4 rejected", error);
}
);
```
结果是:
p4 rejected 2

也证明了你所说的。对于在帖子顶部的那段代码,我想是因为p1、p2、p3的状态都已经改变了,而明显p1的状态是最先改变的,因此p4的状态也随着p1改变了。
而一旦我在p1的exceutor里面延时一下,此时p2、p3的状态都改变了,但是p2的状态是最先改变的,因此p4的状态也随着p2改变了。
otakustay
2015-07-25 20:06:53 +08:00
@haozhang 你观察的结果是对的,但你说的原理是不对的……今天身体不大舒服,明天记得的话可以at我一下我再来详细说这个事儿,蛮复杂的- -
haozhang
2015-07-25 21:50:24 +08:00
@otakustay 嗯,我还想问下你用过Sequelize吗?
otakustay
2015-07-25 22:33:19 +08:00
@haozhang 没有……我是JavaScript专精,但并不会NodeJS
haozhang
2015-07-26 10:35:16 +08:00
@otakustay 可以留一个通讯方式。
otakustay
2015-07-26 19:45:18 +08:00
@haozhang 微博和GitHub都是otakustay

我来说下为啥上面 @haozhang 说的原理是错的。这个问题最根本的一个关键在于:race(或者all等其它和Promise有关的方法)的决定因素不是“Promise状态的变化”,而是“callback的执行”,所以“p1的状态是最先改变”这样的说法完全没用,比如我搞这样的代码:

var p1 = new Promise(function (resolve) { setTimeout(resolve, 1000); });
var p2 = Promise.reject();
setTimeout(function () {
var p3 = Promise.race(p1, p2);
p3.then(logResolved, logRejected);
}, 1500);

这代码里p3会进入resolved状态,但p1显然“比p2更晚改变状态”

所以再重复一次核心:决定因素是callback的执行

那么其原理要研究的就是,callback是啥时候执行的

在上面我贴规范的时候已经有提到一个词,叫enqueueTask。简单来说,就是浏览器有一个全局的任务队列,我们把一个个任务放到队列里,浏览器从队列里拿出来一个执行完再去拿下一个,每个任务的执行都是异步的
当然浏览器其实不一定只有一个队列,任务也不止一种,Promise产生的任务和setTimeout产生的就不一样,这种细节的事不展开说

然后还需要研究的是,在Promise执行过程中,什么时候执行enqueuTask。对于Promise来说也很简单,当then/catch被调用的时候,形成2种可能性:
1. Promise还没有resolve或reject,即处在pending状态,此时会把callback存下来,等进入某个状态时再把对应的callback一一enqueuTask
2. Promise本身已经在resolve或reject状态,则把给到的callback(当然要符合当前状态)直接enqueueTask

到这里这个逻辑其实就很简单了,主楼的代码分析下来是这样的:

1. 创建p1,此时任务队列为空,[]
2. 由于执行new Promise时给的executor函数,p1进入resolved状态,但注意此时并没有任何的callback,所以没有东西执行enqueueTask,队列还是空,[]
3. 创建p2、p3,不多说了,和p1一样,关键就是他们状态虽然是改变了,但根本没有任何callback,所以到此时为止队列依旧是空的,[]
4. 创建p4,用的Promise.race,race方法会分别给传入的Promise们调用then添加callback。所以这一段代码会依次**按顺序**给p1、p2、p3添加resolve和reject的回调,因为3个Promise状态都已经是确定的,所以会执行3次enqueueTask,队列变成[因p1导致p4变resolve, 因p2导致p4变reject, 因p3导致p4变resolve],这3个任务任意一个执行就会导致p4状态变化
5. 给p4挂上2个callback,不过此时p4状态没有改变还是pending,所以这2个callback不会enqueueTask
6. 这段代码执行完了,浏览器算是完成了一个任务,要去获取下一个任务,所以从队列中拿出第1个,即“p1导致p4变resolve”,队列变成2个任务,[因p2导致p4变reject, 因p3导致p4变resolve]
7. 执行拿出来的任务,于是p4被resolve了,且对应的value是p1里的“this is p1”,resolve的同时会把关联的callback放进队列,所以此时队列变成3个任务,[因p2导致p4变reject, 因p3导致p4变resolve, console.log(value)]
8. 继续拿下一个任务,拿出来“因p2导致p4变reject”并执行,但很遗憾p4现在已经是resolve状态了,所以这个任务执行了啥也不会发生,此时队列变成2个任务,[因p3导致p4变resolve, console.log(value)]
9. 下一个任务不多说了,反正p4是不会再变状态了,于是队列里还剩最后一个任务,[console.log(value)]
10. 把这最后一个任务拿出来执行,于是打印出来“this is p1”

JavaScript里凡遇到异步的问题,拿个笔画这个任务队列是最好的理思路的手段
otakustay
2015-07-26 19:46:09 +08:00
v2ex评论不支持markdown对这类技术上的讨论确实很不友好,使用gist虽然能输入代码,但第一不能内联代码段,第二大量的小片段的代码用gist也很影响观看体验
haozhang
2015-07-26 20:07:41 +08:00
@otakustay 嗯,正在研究中,多谢解惑。

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

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

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

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

© 2021 V2EX