一尘不染

如何同步承诺序列?

javascript

我有一个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库中实现了,作为方法序列。该方法可以迭代一系列动态长度,并根据应用程序的业务逻辑的要求创建promise。

后来我把它变成了一个共享库,供所有人使用。


阅读 270

收藏
2020-04-25

共1个答案

一尘不染

这是一些简单的示例,说明如何顺序排列一个数组(依次执行一个异步操作)。

假设您有一系列项目:

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

手动迭代返回承诺

如果您希望从中获得承诺,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返回的promise。

.reduce()的迭代

如果您想用诺言做更多的工作,则可以将所有诺言链接起来:

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

使用.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库具有许多内置的并发控制功能。例如,要通过数组对迭代进行排序,可以使用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异步/等待

如果您在支持异步/等待的环境中进行编码,则也可以只使用常规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()库中的功能非常相似,后者具有一个数组和一个Promise产生函数,并返回一个Promise,该Promise将以一系列已解析的结果进行解析。


@ vitaly-t-
这里有一些关于您的方法的更详细的评论。欢迎您使用最适合您的代码。当我第一次开始使用promise时,我倾向于仅将promise用于他们所做的最简单的事情,而当更高级地使用promise可以为我做更多的事情时,我自己会编写很多逻辑。您只使用自己完全满意的东西,而不希望看到自己熟悉的代码。那可能是人性。

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

以下是有关您的方法的一些特定反馈点:

您在七个地方创造承诺

作为样式的对比,我的代码只有 两个 地方可以显式创建新的Promise-一次在工厂函数中,一次在初始化.reduce()循环中。在其他任何地方,我都只是建立在已经创建的承诺上,方法是将它们链接到它们或在其中返回值,或者直接将它们返回。您的代码有
七个 创建承诺的独特位置。现在,良好的编码并不是查看可以创建承诺的地方数量的竞争,但是这可能表明利用已创建的承诺与测试条件以及创建新的承诺的区别。

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

承诺是安全的。这意味着在承诺处理程序中引发的异常将自动拒绝该承诺。如果您只是想让异常成为拒绝,那么这是一个非常有用的功能。实际上,您会发现仅丢掉自己是从处理程序中拒绝而不创建另一个承诺的有用方法。

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

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

兑现承诺

如果您不知道某物是否返回了承诺,则可以将其转换为承诺。然后,promise库将自行检查是否是一个Promise,甚至是与您使用的Promise库匹配的Promise类型,如果不是,则将其包装为一个。这样可以省去您自己重写很多这种逻辑的麻烦。

履行承诺的合同

如今,在许多情况下,为一个可能执行异步操作以返回承诺的功能签定合同是完全可行的。如果函数只是想做同步的事情,那么它可以返回一个已解决的Promise。您似乎觉得这很麻烦,但这绝对是顺风顺水的方式,我已经写了很多要求这样做的代码,一旦您熟悉了诺言,就感觉很自然。它抽象出操作是同步还是异步,并且调用者不必知道或做任何特殊的事情。这是对promises的很好使用。

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

可以编写工厂函数来仅创建一个承诺,然后解决或拒绝它。这种样式还使其安全抛出,因此工厂功能中发生的任何异常都会自动变为拒绝。这也使合同始终自动返回承诺。

虽然我意识到这个工厂函数是一个占位符函数(它甚至不执行任何异步操作),但希望您能看到考虑它的样式:

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则将拒绝。因此,您无需使用拒绝创建新的承诺。没有拒绝处理程序的简单代码可以用更少的代码完成相同的操作。


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

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

关于此实现的一些评论:

  1. Promise.resolve(factory(idx))本质上将结果factory(idx)变成了承诺。如果它只是一个值,那么它将变成一个已解决的承诺,并将该返回值作为解决值。如果已经是一个承诺,那么它就只是兑现了那个承诺。因此,它将在factory()函数的返回值上替换所有类型检查代码。

  2. 工厂函数通过返回null或保证谁的已解决值的诺言最终表示是,表明已完成此操作null。上面的转换将这两个条件映射到相同的结果代码。

  3. 工厂函数自动捕获异常并将其转换为拒绝,然后由该sequence()函数自动处理。如果您只是想中止处理并在第一个异常或拒绝时将错误反馈给错误,则使promises可以处理大量错误是一个重要的优势。

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

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

  6. 这使用了与您类似的方法(有意使用通用架构)来链接到的多个调用loop()

2020-04-25