如何同步一系列Promise?

IT技术 javascript promise
2021-01-14 05:32:42

我有一个 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得到这样的数组元素的结果,而不是它们的执行,因为它是在这样的例子中预定义的。

但是,如何以这种方式生成一系列 Promise 以避免过早执行呢?

这是一个修改后的示例:

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

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

解决方案

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

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

6个回答

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

假设您有一组项目:

var arr = [...];

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

并且,假设您有一个用于处理数组中的一项的Promise返回函数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);

手动迭代返回Promise

如果您想要返回一个PromiseprocessArray()以便知道何时完成,您可以将其添加到其中:

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 返回的Promise。

使用 .reduce() 进行迭代

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

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

注意:这将在第一次拒绝时停止链并将该原因传递回从 返回的PromiseprocessArray()

对于成功场景,返回的PromiseprocessArray()将使用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
});

工作演示: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 库进行迭代

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 的环境中编码,您也可以只使用常规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
});

仅供参考,我认为我processArray()在这里的函数与Promise.map()Bluebird promise 库中的函数非常相似,它接受一个数组和一个 promise 生成函数,并返回一个使用已解析结果数组进行解析的 promise。


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

我会建议,随着我对 Promise 可以为我做什么的了解越来越多,我现在喜欢编写使用更多 Promise 高级功能的代码,这对我来说似乎很自然,我觉得我正在构建良好的基础经过测试的基础设施具有许多有用的功能。我只要求你在学习越来越多的知识时保持开放的心态,以便朝着那个方向前进。我认为,随着您的理解提高,迁移是一个有用且富有成效的方向。

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

你在七个地方创建Promise

作为风格的对比,我的代码只有两个地方我显式地创建了一个新的Promise - 一次在工厂函数中,一次用于初始化.reduce()循环。在其他地方,我只是通过链接到它们或返回其中的值或直接返回它们来构建已经创建的Promise。您的代码有七个独特的地方,您可以在其中创建Promise。现在,好的编码不是看你能在多少地方创建Promise的比赛,但这可能表明利用已经创建的Promise与测试条件和创建新Promise的差异。

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

Promise 是抛出安全的。这意味着Promise处理程序中抛出的异常将自动拒绝该Promise。如果你只是想让异常变成拒绝,那么这是一个非常有用的特性。事实上,你会发现直接抛出自己是一种从处理程序中拒绝而不创建另一个Promise的有用方法。

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

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

投向Promise

如果您不知道某些东西是否返回了Promise,那么您可以将其转换为Promise。然后,promise 库会自己检查它是否是一个promise,甚至它是否与您正在使用的promise 库相匹配,如果不是,则将其包装成一个。这样可以省去自己重写很多这样的逻辑。

返回Promise的合同

在如今的许多情况下,为可能执行异步操作以返回Promise的函数签订合同是完全可行的。如果函数只是想做一些同步的事情,那么它可以只返回一个已解决的Promise。你似乎觉得这很繁重,但这绝对是风吹草动的方式,我已经写了很多需要这样做的代码,一旦你熟悉了 promises 就会感觉很自然。它抽象出操作是同步还是异步,并且调用者不必知道或以任何方式做任何特殊的事情。这是 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;
        }
    });
}

如果这些操作中的任何一个是异步的,那么他们可以只返回自己的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($.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.then()处理程序返回的内容拒绝,obj则将拒绝。所以你不需要用拒绝创建一个新的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("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);
});

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

关于这个实现的一些评论:

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

  2. 工厂函数通过返回null其解析值最终或 的Promise来表示它已完成null上面的转换将这两个条件映射到相同的结果代码。

  3. 工厂函数自动捕获异常并将它们转换为拒绝,然后由sequence()函数自动处理如果您只想中止处理并将错误反馈给第一个异常或拒绝,那么这是让 Promise 进行大量错误处理的一个显着优势。

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

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

  6. 这使用与您类似的方法(有意尝试保持您的一般架构)将多个调用链接到loop().

@jfriend00 你是男人!多年来一直在寻找这样的东西。清晰而详细的答案。谢谢!
2021-03-16 05:32:42
我不接受我自己的答案的唯一原因 - StackOverflow 在发布后的 3 天内不允许它,这对我来说没有意义,但在这里,我仍然不能,必须等待。 ..
2021-03-27 05:32:42
@vitaly-t - 我只是想贡献新的想法(这就是 StackOverflow 的工作原理)。你还没有接受任何答案,所以我认为你也特别愿意接受想法。我所有的代码示例都在第一次拒绝时停止。我的第二个和第三个选项还将拒绝原因传播回返回的Promise。而且,最后,我不知道“解析操作的最终结果”是什么意思。我在你的回答中没有看到解释这一点的例子,所以我不知道这是一个要求或理解它是什么。请解释该要求,我可以适应。
2021-03-28 05:32:42
@vitaly-t - 我不知道这些答案中的一个是来自你的(在 SO 上这是一件很容易的事情)。看了你的解决方案后,我建议你研究一下我的矿石。您似乎正在重新实现许多Promise已经为您处理的东西(例如 throw-safety)。老实说,您的代码看起来比需要的更复杂(如果这对您很重要)。而且,当您完全控制该函数并且它总是返回一个Promise时,还不清楚为什么要测试工厂函数的返回值。
2021-04-01 05:32:42
@vitaly-t - 好的,最后一点反馈。我在答案的末尾添加了一些关于您的实现的非常具体的反馈点,然后提供了一个使用您的一般结构的新实现,但尝试合并这些反馈点。您显然可以自由使用您最喜欢的任何代码,但我认为值得解释不同实现背后的一些逻辑。我希望你只把它当作一种思考/学习练习。仅此而已。
2021-04-02 05:32:42

Promise 表示操作的,而不是操作本身。操作已经开始,所以你不能让他们互相等待。

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

只是想知道如何解决它,我更新了问题。
2021-03-11 05:32:42

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

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

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

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

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

当一个或多个Promise失败时,可能会出现问题。

我理解我得到的两个答案,因此稍微更新了我的问题,以便我们可以更接近正确的解决方案。
2021-03-11 05:32:42
作为记录,您的答案是正确的,并且比我的答案更早-我知道这会多么令人沮丧(首先,正确答案只是为了没有投票而另一个人得到了一些)-我花了一年的时间在Promise标签上了解如何更好地编写人们相关的答案,而无需从根本上理解Promise。拜托,请多来,并在标签上回答更多我很感激,我们需要更多的人来照顾标签。顺便说一句,这是一个+1。
2021-03-15 05:32:42
@BenjaminGruenbaum - 谢谢..我不在这里是因为票数高。我更喜欢提前付费的方法!
2021-04-08 05:32:42

尽管非常密集,但这里有另一种解决方案,它将在一组值上迭代一个返回Promise的函数并使用一组结果进行解析:

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

请注意,因为我们从一个 promise 减少到下一个 promise,所以每个项目都是按顺序处理的。

它在功能上等同于来自@jfriend00 的“使用 .reduce() 解决的数组迭代”解决方案,但更简洁一些。

我想有两种方法来处理这个问题:

  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(() => { console.log('first');console.log(new Date());resolve() }, 1000)
  })

  const promise2 = new Promise(resolve => {
    setTimeout(() => {console.log('second');console.log(new Date());  resolve() }, 3000)
  })

  const promise3 = new Promise(resolve => {
    setTimeout(() => { console.log('third');console.log(new Date()); resolve() }, 7000)
  })

  const promises = [promise1, promise2, promise3]

  await Promise.all(promises)

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