V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
haozhang
V2EX  ›  程序员

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

  •  1
     
  •   haozhang ·
    Summerlve · 2015-07-25 12:00:55 +08:00 · 4400 次点击
    这是一个创建于 3404 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我看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了。

    29 条回复    2015-07-31 11:22:05 +08:00
    df4VW
        1
    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
        2
    haozhang  
    OP
       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
        3
    yoa1q7y  
       2015-07-25 12:10:32 +08:00
    可以看下这篇文章,仔细研究一下
    http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/
    oott123
        4
    oott123  
       2015-07-25 12:13:12 +08:00
    p1 在创建的时候就被执行了,并不一定等到你 race 才开始。
    haozhang
        5
    haozhang  
    OP
       2015-07-25 12:13:53 +08:00
    @oott123 明显zakas不认同你这句话。
    df4VW
        6
    df4VW  
       2015-07-25 12:16:06 +08:00
    @haozhang [p2, p1, p3] => 2
    haozhang
        7
    haozhang  
    OP
       2015-07-25 12:24:05 +08:00
    @df4VW 数组换个位置结果就变了...不觉得很奇怪嘛...
    oott123
        8
    oott123  
       2015-07-25 12:24:44 +08:00
    haozhang
        9
    haozhang  
    OP
       2015-07-25 12:57:12 +08:00
    @oott123 测试结果是这样的...但是zakas讲的不是这样的...zakas认为p2改变了p4,而不是p1改变了p4.
    df4VW
        10
    df4VW  
       2015-07-25 13:05:53 +08:00
    @haozhang 因为是根据数组遍历的顺序的啊。。不奇怪啊,你看我一楼说的啊
    haozhang
        11
    haozhang  
    OP
       2015-07-25 13:53:29 +08:00
    @df4VW 你说的是one of the promises in the iterable resolves or rejects , 哪里说按照数组的顺序来了。
    otakustay
        12
    otakustay  
       2015-07-25 14:22:12 +08:00   ❤️ 1
    从我现在为止对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
        13
    haozhang  
    OP
       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
        14
    otakustay  
       2015-07-25 20:06:53 +08:00   ❤️ 1
    @haozhang 你观察的结果是对的,但你说的原理是不对的……今天身体不大舒服,明天记得的话可以at我一下我再来详细说这个事儿,蛮复杂的- -
    haozhang
        15
    haozhang  
    OP
       2015-07-25 21:50:24 +08:00
    @otakustay 嗯,我还想问下你用过Sequelize吗?
    otakustay
        16
    otakustay  
       2015-07-25 22:33:19 +08:00
    @haozhang 没有……我是JavaScript专精,但并不会NodeJS
    haozhang
        17
    haozhang  
    OP
       2015-07-26 10:35:16 +08:00
    @otakustay 可以留一个通讯方式。
    otakustay
        18
    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
        19
    otakustay  
       2015-07-26 19:46:09 +08:00
    v2ex评论不支持markdown对这类技术上的讨论确实很不友好,使用gist虽然能输入代码,但第一不能内联代码段,第二大量的小片段的代码用gist也很影响观看体验
    haozhang
        20
    haozhang  
    OP
       2015-07-26 20:07:41 +08:00
    @otakustay 嗯,正在研究中,多谢解惑。
    haozhang
        21
    haozhang  
    OP
       2015-07-26 23:43:15 +08:00
    @otakustay 我的理解是:Promise.race()和Promise.all()方法看的是,谁先调用race或者all添加的回调函数,也就是race/all产生的promise对象的状态是根据谁先调用回调函数来判断的(这个回调函数指的是race/all添加的回调函数),根据这个理解,我仿写了一个race函数,你看看我的理解对不对:

    var race = (array) => {
    return new Promise((resolve, reject) => {
    array.map((cur) => {
    cur
    .then(
    (value) => {
    resolve(value);
    },
    (error) => {
    reject(error);
    }
    );
    });
    });
    };
    haozhang
        22
    haozhang  
    OP
       2015-07-26 23:45:03 +08:00
    haozhang
        23
    haozhang  
    OP
       2015-07-26 23:45:55 +08:00
    我受不了了....你看下面的代码吧,上面贴的代码格式太差了。
    haozhang
        24
    haozhang  
    OP
       2015-07-26 23:54:42 +08:00
    otakustay
        25
    otakustay  
       2015-07-27 11:52:27 +08:00
    没错,race就是这么搞的,.map可以改成.each,更简单的写法:

    Promise.race = (array) => new Promise((resolve, reject) => array.forEach((promise) => promise.then(resolve, reject)));

    all要更麻烦些,有参数收集等
    haozhang
        26
    haozhang  
    OP
       2015-07-27 14:19:31 +08:00
    ysmood
        27
    ysmood  
       2015-07-31 06:21:23 +08:00
    @otakustay race 和 all 的实现都不完全正确,或者不符合 ES6 的规范。数组的每个 item 可以不是 promise 的,应该用 resolve wrap 起来,比如 race 的例子:

    Promise.race = (arr) => new Promise((resolve, reject) => arr.forEach((v) => Promise.resolve(v).then(resolve, reject)));

    另外不要忘记判断 arr 是否是 iterable 类型。虽然有点吹毛求疵了 XD
    haozhang
        28
    haozhang  
    OP
       2015-07-31 07:24:48 +08:00 via iPhone
    @ysmood →_→ 我只是大致实现了一下
    otakustay
        29
    otakustay  
       2015-07-31 11:22:05 +08:00
    @ysmood 我懂,但是我懒得在v2上敲这么多代码啊- -
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3807 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 10:30 · PVG 18:30 · LAX 02:30 · JFK 05:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.