如何同步一系列承诺?

vit*_*y-t 55 javascript promise

我有一个promise对象数组,必须按照它们在数组中列出的相同顺序进行解析,即我们不能尝试解析一个元素,直到前一个元素被解析(如方法所示Promise.all([...])).

如果一个元素被拒绝,我需要链接立即拒绝,而不尝试解析以下元素.

我该如何实现这个,或者是否存在这种sequence模式的现有实现?

function sequence(arr) {
    return new Promise(function (resolve, reject) {
        // try resolving all elements in 'arr',
        // but strictly one after another;
    });
}
Run Code Online (Sandbox Code Playgroud)

编辑

初始答案表明我们只能sequence得到这些数组元素的结果,而不是它们的执行结果,因为它是在这样的例子中预定义的.

但是,如何以避免早期执行的方式生成一系列承诺呢?

这是一个修改过的例子:

function sequence(nextPromise) {
    // while nextPromise() creates and returns another promise,
    // continue resolving it;
}
Run Code Online (Sandbox Code Playgroud)

我不想把它变成一个单独的问题,因为我认为它是同一个问题的一部分.

下面的一些答案和随后的讨论有点误入歧途,但最终解决方案完全符合我的要求,是在spex库中实现的,作为方法序列.该方法可以迭代一系列动态长度,并根据应用程序的业务逻辑创建promise.

后来我把它变成了一个供大家使用的共享库.

jfr*_*d00 123

下面是一些简单的示例,说明如何按序列顺序执行每个异步操作的数组(一个接一个).

我们假设您有一系列项目:

var arr = [...];
Run Code Online (Sandbox Code Playgroud)

并且,您希望对阵列中的每个项目执行特定的异步操作,一次一个地连续执行,以便下一个操作在前一个操作完成之前不会启动.

并且,假设您有一个promise函数来处理数组中的一个项目fn(item):

手动迭代

function processItem(item) {
    // do async operation and process the result
    // return a promise
}
Run Code Online (Sandbox Code Playgroud)

然后,你可以做这样的事情:

function processArray(array, fn) {
    var index = 0;

    function next() {
        if (index < array.length) {
            fn(array[index++]).then(next);
        }
    }
    next();
}

processArray(arr, processItem);
Run Code Online (Sandbox Code Playgroud)

手动迭代返回承诺

如果你想要一个承诺,processArray()你知道什么时候完成,你可以添加它:

function processArray(array, fn) {
    var index = 0;

    function next() {
        if (index < array.length) {
            return fn(array[index++]).then(function(value) {
                // apply some logic to value
                // you have three options here:
                // 1) Call next() to continue processing the result of the array
                // 2) throw err to stop processing and result in a rejected promise being returned
                // 3) return value to stop processing and result in a resolved promise being returned
                return next();
            });
        }
    } else {
        // return whatever you want to return when all processing is done
        // this returne value will be the ersolved value of the returned promise.
        return "all done";
    }
}

processArray(arr, processItem).then(function(result) {
    // all done here
    console.log(result);
}, function(err) {
    // rejection happened
    console.log(err);
});
Run Code Online (Sandbox Code Playgroud)

注意:这将在第一次拒绝时停止链并将该原因传递回processArray返回的promise.

用.reduce()迭代

如果你想用promises做更多的工作,你可以链接所有的承诺:

function processArray(array, fn) {
   return array.reduce(function(p, item) {
       return p.then(function() {
          return fn(item);
       });
   }, Promise.resolve());
}

processArray(arr, processItem).then(function(result) {
    // all done here
}, function(reason) {
    // rejection happened
});
Run Code Online (Sandbox Code Playgroud)

注意:这将在第一次拒绝时停止链并将该原因传回给从中返回的承诺processArray().

对于成功方案,返回的promise processArray()将使用fn回调的最后一个解析值来解决.如果你想累积一个结果列表并用它解决,你可以从闭包数组中收集结果,fn并且每次都继续返回该数组,这样最终的解析就是一个结果数组.

使用与数组一起解析的.reduce()进行迭代

并且,因为现在看来你希望最终的promise数据是一个数组(按顺序),这里是以前的解决方案的修订版,它产生了:

function processArray(array, fn) {
   var results = [];
   return array.reduce(function(p, item) {
       return p.then(function() {
           return fn(item).then(function(data) {
               results.push(data);
               return results;
           });
       });
   }, Promise.resolve());
}

processArray(arr, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});
Run Code Online (Sandbox Code Playgroud)

工作演示:http://jsfiddle.net/jfriend00/h3zaw8u8/

一个显示拒绝的工作演示:http://jsfiddle.net/jfriend00/p0ffbpoc/

使用.reduce()进行迭代,使用延迟解析数组

并且,如果要在操作之间插入一小段延迟:

function delay(t, v) {
    return new Promise(function(resolve) {
        setTimeout(resolve.bind(null, v), t);
    });
}

function processArrayWithDelay(array, t, fn) {
   var results = [];
   return array.reduce(function(p, item) {
       return p.then(function() {
           return fn(item).then(function(data) {
               results.push(data);
               return delay(t, results);
           });
       });
   }, Promise.resolve());
}

processArray(arr, 200, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});
Run Code Online (Sandbox Code Playgroud)

与Bluebird Promise Library的迭代

Bluebird promise库具有很多内置的并发控制功能.例如,要通过数组进行迭代,您可以使用Promise.mapSeries().

Promise.mapSeries(arr, function(item) {
    // process each individual item here, return a promise
    return processItem(item);
}).then(function(results) {
    // process final results here
}).catch(function(err) {
    // process array here
});
Run Code Online (Sandbox Code Playgroud)

或者在迭代之间插入延迟:

Promise.mapSeries(arr, function(item) {
    // process each individual item here, return a promise
    return processItem(item).delay(100);
}).then(function(results) {
    // process final results here
}).catch(function(err) {
    // process array here
});
Run Code Online (Sandbox Code Playgroud)

使用ES7 async/await

如果您在支持async/await的环境中进行编码,您也可以在for循环中使用常规循环然后await使用promise,这将导致for循环暂停,直到在继续之前解析了promise.这将有效地对您的异步操作进行排序,以便下一个操作在上一个操作完成之前不会启动.

async function processArray(array, fn) {
    let results = [];
    for (let i = 0; i < array.length; i++) {
        let r = await fn(array[i]);
        results.push(r);
    }
    return results;    // will be resolved value of promise
}

// sample usage
processArray(arr, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});
Run Code Online (Sandbox Code Playgroud)

仅供参考,我认为我的processArray()函数非常类似于Promise.map()Bluebird promise库,它接受一个数组和一个promise生成函数,并返回一个使用一系列已解析结果解析的promise.


@ vitaly-t - 这里有一些关于你的方法的更详细的评论.欢迎您使用最适合您的代码.当我第一次开始使用promises时,我倾向于仅使用promises来完成他们所做的最简单的事情,并且当更高级的promises使用可以为我做更多的事情时,我自己写了很多逻辑.你只使用你完全熟悉的东西,除此之外,你宁愿看到你熟悉的自己的代码.这可能是人性.

我会建议,随着我越来越了解承诺可以为我做什么,我现在想编写使用更多承诺的高级功能的代码,这对我来说似乎很自然,我觉得我建立得很好经过测试的基础设施,具有许多有用的功 我只是要求你保持开放的态度,因为你越来越多地学习这个方向.我认为,随着您的理解的提高,迁移是一个有用且富有成效的方向.

以下是您的方法的一些具体反馈点:

你在七个地方创造了承诺

作为样式的对比,我的代码只有两个地方,我明确地创建一个新的承诺 - 一次在工厂函数和一次初始化.reduce()循环.在其他任何地方,我只是建立在已经通过链接到它们或在其中返回值或直接返回它们而创建的promise.您的代码有七个独特的位置,您可以在其中创建承诺.现在,良好的编码不是一个竞争,看你有多少地方可以创造一个承诺,但这可能指出杠杆的差异已经创建的承诺与测试条件和创造新的承诺.

投掷安全是一个非常有用的功能

承诺是安全的.这意味着在promise处理程序中抛出的异常将自动拒绝该promise.如果您只是希望异常成为拒绝,那么这是一个非常有用的功能,可以利用.事实上,你会发现只是抛弃自己是一种有用的方法来拒绝处理程序而不创建另一个承诺.

很多Promise.resolve()Promise.reject()可能是简化的机会

如果您看到包含大量Promise.resolve()Promise.reject()语句的代码,那么可能有机会更好地利用现有的承诺,而不是创建所有这些新的承诺.

屈服于承诺

如果您不知道某件事是否返回了承诺,那么您可以将其投入承诺.然后,promise库会自行检查它是否是一个承诺,甚至它是否是与您正在使用的promise库相匹配的承诺,如果没有,则将其包装成一个.这可以节省自己重写的大量逻辑.

承诺回报承诺

在许多情况下,现在,拥有一个函数的契约是完全可行的,这个函数可以做一些异步的事情来返回一个promise.如果函数只是想做同步的事情,那么它只能返回一个已解决的promise.你似乎觉得这很麻烦,但它绝对是风吹的方式,我已经写了很多需要的代码,一旦你熟悉了承诺,感觉就很自然了.它抽象出操作是同步还是异步,并且调用者不必知道或做任何特殊的事情.这是对承诺的好用.

可以编写工厂函数以仅创建一个承诺

可以编写工厂函数以仅创建一个承诺,然后解析或拒绝它.这种风格也使它安全,因此工厂函数中出现的任何异常都会自动变为拒绝.它还使合同始终自动返回承诺.

虽然我意识到这个工厂函数是一个占位符函数(它甚至不做任何异步),希望你能看到它的风格:

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve("one");
                break;
            case 1:
                resolve("two");
                break;
            case 2:
                resolve("three");
                break;
            default:
                resolve(null);
                break;
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

如果这些操作中的任何一个都是异步的,那么他们就可以返回自己的承诺,这些承诺将自动链接到这样的一个中心承诺:

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve($.ajax(...));
            case 1:
                resole($.ajax(...));
            case 2:
                resolve("two");
                break;
            default:
                resolve(null);
                break;
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

return promise.reject(reason)不需要使用拒绝处理程序

当你有这个代码体时:

    return obj.then(function (data) {
        result.push(data);
        return loop(++idx, result);
    }, function (reason) {
        return promise.reject(reason);
    });
Run Code Online (Sandbox Code Playgroud)

拒绝处理程序不添加任何值.你可以改为:

    return obj.then(function (data) {
        result.push(data);
        return loop(++idx, result);
    });
Run Code Online (Sandbox Code Playgroud)

你已经回来了obj.then().如果obj拒绝或者如果链接到obj或从.then()处理程序拒绝返回任何内容,则obj拒绝.因此,您无需使用拒绝创建新承诺.没有拒绝处理程序的更简单的代码用更少的代码做同样的事情.


这是代码的一般体系结构中的一个版本,它试图包含大多数这些想法:

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve("zero");
                break;
            case 1:
                resolve("one");
                break;
            case 2:
                resolve("two");
                break;
            default:
                // stop further processing
                resolve(null);
                break;
        }
    });
}


// Sequentially resolves dynamic promises returned by a factory;
function sequence(factory) {
    function loop(idx, result) {
        return Promise.resolve(factory(idx)).then(function(val) {
            // if resolved value is not null, then store result and keep going
            if (val !== null) {
                result.push(val);
                // return promise from next call to loop() which will automatically chain
                return loop(++idx, result);
            } else {
                // if we got null, then we're done so return results
                return result;
            }
        });
    }
    return loop(0, []);
}

sequence(factory).then(function(results) {
    log("results: ", results);
}, function(reason) {
    log("rejected: ", reason);
});
Run Code Online (Sandbox Code Playgroud)

工作演示:http://jsfiddle.net/jfriend00/h3zaw8u8/

关于此实现的一些评论:

  1. Promise.resolve(factory(idx))基本上将结果factory(idx)转化为承诺.如果它只是一个值,则它将成为已解决的承诺,并将该返回值作为解析值.如果它已经是一个承诺,那么它只是链接到那个承诺.因此,它会替换factory()函数返回值上的所有类型检查代码.

  2. 工厂功能通过返回任何一个null或一个已解决的值最终成为的承诺来表明它是完成的null.上面的转换将这两个条件映射到相同的结果代码.

  3. 工厂函数自动捕获异常并将其转换为拒绝,然后由sequence()函数自动处理.如果您只是想要中止处理并在第一个异常或拒绝时反馈错误,那么让promises执行大量错误处理是一个显着优势.

  4. 此实现中的工厂函数可以返回promise或静态值(用于同步操作),它可以正常工作(根据您的设计请求).

  5. 我已经在工厂函数中的promise回调中使用抛出异常对其进行了测试,它确实只是拒绝并传播该异常以拒绝序列承诺,并将异常作为原因.

  6. 这使用了与您类似的方法(故意,试图保持一般的体系结构)来链接多个调用loop().

  • @ vitaly-t - 好的,最后一点反馈.我在回答的最后添加了一些关于您的实现的非常具体的反馈意见,然后提供了一个使用您的一般结构的新实现,但尝试将这些反馈点纳入其中.您显然可以自由地使用您最喜欢的任何代码,但我认为值得解释一些不同实现背后的逻辑.我希望你把它当作一个思想/学习练习.没有比它更重要的了. (3认同)

Ben*_*aum 11

Promise代表操作的,而不是操作本身.操作已经开始,所以你不能让他们彼此等待.

相反,您可以同步返回按顺序调用它们的promises的函数(例如通过带有promise链接的循环),或者使用.eachbluebird中的方法.


Ami*_*ich 5

您不能简单地运行X异步操作,然后希望它们按顺序解析.

执行此类操作的正确方法是仅在解析之前运行新的异步操作:

doSomethingAsync().then(function(){
   doSomethingAsync2().then(function(){
       doSomethingAsync3();
       .......
   });
});
Run Code Online (Sandbox Code Playgroud)

编辑
似乎您想要等待所有承诺,然后按特定顺序调用它们的回调.像这样的东西:

var callbackArr = [];
var promiseArr = [];
promiseArr.push(doSomethingAsync());
callbackArr.push(doSomethingAsyncCallback);
promiseArr.push(doSomethingAsync1());
callbackArr.push(doSomethingAsync1Callback);
.........
promiseArr.push(doSomethingAsyncN());
callbackArr.push(doSomethingAsyncNCallback);
Run Code Online (Sandbox Code Playgroud)

然后:

$.when(promiseArr).done(function(promise){
    while(callbackArr.length > 0)
    {
       callbackArr.pop()(promise);
    }
});
Run Code Online (Sandbox Code Playgroud)

这可能发生的问题是一个或多个承诺失败.