如何同步一系列承诺?

时间:2015-04-26 17:07:39

标签: 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;
    });
}

修改

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

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

这是一个修改过的例子:

function sequence(nextPromise) {
    // while nextPromise() creates and returns another promise,
    // continue resolving it;
}

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

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

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

6 个答案:

答案 0 :(得分:112)

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

假设您有一系列项目:

var arr = [...];

并且,您希望对阵列中的每个项目执行特定的异步操作,一次一个,以便下一个操作在前一个操作完成之前不会开始。

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

手动迭代

function processItem(item) {
    // do async operation and process the result
    // return a promise
}

然后,你可以这样做:

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

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

processArray(arr, processItem);

手动迭代返回承诺

如果您希望从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);
});

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

使用.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
});

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

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

使用.reduce()解析数组

而且,既然现在看来您希望最终的承诺结果是一个数据数组(按顺序),这里是以前解决方案的修订版,它产生了:

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
});

工作演示: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
});

使用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
});

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

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
});

使用ES7 async / await

如果你在支持async / await的环境中进行编码,你也可以在循环中使用常规for循环然后await一个承诺,它将导致{ {1}}循环暂停,直到承诺得到解决后再继续。这将有效地对您的异步操作进行排序,以便下一个操作不会开始,直到上一个操作完成。

for

仅供参考,我认为我的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 }); 函数与Bluebird promise库中的Promise.map()非常相似,它接受一个数组和一个promise生成函数,并返回一个使用一系列已解析结果解析的promise

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

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

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

您在七个地方创建承诺

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

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

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

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

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

投放承诺

如果你不知道某件事是否有回应,那么你可以将其投入承诺。然后,承诺库会自行检查它是否是承诺,甚至它是否是与您使用的承诺库匹配的承诺类型,如果不是,则包装它合而为一。这可以节省你自己重写的大部分逻辑。

回复承诺的合同

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

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

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

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

Promise.reject()

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

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;
        }
    });
}

不需要使用拒绝处理程序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; } }); }

当你拥有这段代码时:

return promise.reject(reason)

拒绝处理程序不添加任何值。你可以这样做:

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

您已经返回 return obj.then(function (data) { result.push(data); return loop(++idx, result); }); 的结果。如果obj.then()拒绝或有任何链接到obj或从obj处理程序拒绝返回的内容,则.then()将拒绝。因此,您不需要使用拒绝创建新的承诺。没有拒绝处理程序的更简单的代码用更少的代码做同样的事情。

这是您的代码的一般架构中的一个版本,它试图包含大部分这些想法:

obj

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

关于此实施的一些评论:

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

  2. 工厂功能通过返回factory()或已解决的值最终为null的承诺来表示完成。上面的演员将这两个条件映射到相同的结果代码。

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

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

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

  6. 这会使用与您类似的方法(故意,尝试使用您的常规架构)将多个来电链接到sequence()

答案 1 :(得分:11)

Promise代表操作的,而不代表操作本身。操作已经启动,因此您无法让他们彼此等待。

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

答案 2 :(得分:5)

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

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

doSomethingAsync().then(function(){
   doSomethingAsync2().then(function(){
       doSomethingAsync3();
       .......
   });
});

编辑左看似乎要等待所有承诺,然后按特定顺序调用回调。像这样:

var callbackArr = [];
var promiseArr = [];
promiseArr.push(doSomethingAsync());
callbackArr.push(doSomethingAsyncCallback);
promiseArr.push(doSomethingAsync1());
callbackArr.push(doSomethingAsync1Callback);
.........
promiseArr.push(doSomethingAsyncN());
callbackArr.push(doSomethingAsyncNCallback);

然后:

$.when(promiseArr).done(function(promise){
    while(callbackArr.length > 0)
    {
       callbackArr.pop()(promise);
    }
});

这可能导致的问题是一个或多个承诺失败。

答案 3 :(得分:3)

虽然非常密集,但是这是另一个解决方案,它将在值数组上迭代一个promise-returns函数,并使用一系列结果解析:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

用法:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

请注意,由于我们会从一个承诺减少到下一个承诺,因此每个项目都会按顺序处理。

它在功能上等同于&#34;迭代与.reduce()解析数组&#34;来自@ jfriend00的解决方案,但有点整洁。

答案 4 :(得分:0)

我想有两种方法可以解决这个问题:

  1. 创建多个promise,并按以下方式使用allWithAsync函数:
let allPromiseAsync = (...PromisesList) => {
return new Promise(async resolve => {
    let output = []
    for (let promise of PromisesList) {
        output.push(await promise.then(async resolvedData => await resolvedData))
        if (output.length === PromisesList.length) resolve(output)
    }
}) }
const prm1= Promise.resolve('first');
const prm2= new Promise((resolve, reject) => setTimeout(resolve, 2000, 'second'));
const prm3= Promise.resolve('third');

allPromiseAsync(prm1, prm2, prm3)
    .then(resolvedData => {
        console.log(resolvedData) // ['first', 'second', 'third']
    });
  1. 改为使用Promise.all函数:
  (async () => {
  const promise1 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 2500)
  })

  const promise2 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 5000)
  })

  const promise3 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 1000)
  })

  const promises = [promise1, promise2, promise3]

  await Promise.all(promises)

  console.log('This line is shown after 8500ms')
})()

答案 5 :(得分:0)

在我看来,您应该使用for循环(是,这是我唯一推荐的for循环)。原因是,当您使用for循环时,它允许您await进行循环的每个迭代,其中使用reducemapforEach并全部运行您的诺言同时发生。听起来不是您想要的,您希望每个诺言都等到之前的诺言解决。因此,您可以执行以下操作。

const ids = [0, 1, 2]
const accounts = ids.map(id => getId(id))
const accountData = async() => {
   for await (const account of accounts) {
       // account will equal the current iteration of the loop
       // and each promise are now waiting on the previous promise to resolve! 
   }
}

// then invoke your function where ever needed
accountData()

显然,如果您想变得极端,可以执行以下操作:

 const accountData = async(accounts) => {
    for await (const account of accounts) {
       // do something
    }
 }

 accountData([0, 1, 2].map(id => getId(id)))

与其他任何示例相比,它更具可读性,代码少得多,减少了此功能所需的行数,遵循了一种更具功能性的编程方式,并充分利用了ES7! !!!

还取决于您的设置或在阅读本文时,您可能需要添加plugin-proposal-async-generator-functions polyfill或看到以下错误

@babel/plugin-proposal-async-generator-functions (https://git.io/vb4yp) to the 'plugins' section of your Babel config to enable transformation.

相关问题