结合同步代码和承诺的正确方法是什么?

What is the proper way to combine synchronous code and promises?

我正在编写一个函数,用作实际服务器 API 调用和用户空间之间的 API 中介函数。 这个函数验证一些输入数据(同步),适当地转换它(同步),读取一些其他数据(异步)。然后将结果合并并用于最终对服务器端点进行实际调用(异步)。 此函数必须始终 return 一个承诺,使用它的更高级别 API 中的所有执行流程都应通过承诺链处理。

但是,我无法确定我是否以正确的方式进行操作。还有,我有一些相关的问题,我找不到答案。

这是我到目前为止写的代码:

import axios from "axios";
import * as MyAPI from "./MyAPI";
import querystring from "querystring";

function fetchFromServerEndpoint(route, routeParams, optQueryParams) {
    // Is it ok to use Promise.all() to combine sync and async code as below?
    return Promise.all([
        new Promise((resolve, reject) => {
            const routeReplacedParams = route.replace(/{([^{/]*?)}/g, (match, paramName) => {
                const paramValue = routeParams[paramName];

                if (paramValue === undefined) {
                    // Here I need to terminate execution of the replacer function
                    // and reject this promise
                    // Is it ok to `throw` here?
                    throw "UNDEFINED_ROUTE_PARAMETER";

                    // Can I somehow `return reject(...)` here instead of throw-ing?
                }

                return paramValue;
            });

            return resolve(routeReplacedParams);
        }),
        // Is it fine to use async to mock a promise with synchronous code?
        async function(){
            return querystring.stringify(optQueryParams);
        }),
        // This is part of a custom API, returns a legit promise
        MyAPI.getAuthorizationAsync(),
    ]).then(([finalRoute, queryParams, authToken]) => {
        // axios library, all APIs will return a promise
        return axios.get(`${finalRoute}?${queryParams}`, {
            headers: {
                Authorization: `Basic ${authToken}`
            }
        });
    });
}

我的问题是:

  1. 当同步函数出现错误情况时,在 promise 中停止执行代码块的正确方法是什么?此外,就像在 String.replace 的 replacer 函数中一样,'throw' 可以吗?你能建议更好的选择,或者更友好的方法吗?
  2. async 标记函数将隐含地创建一个承诺,并且还将隐含地将(未捕获的)throws 转换为 Promise.reject()s 和 returns intro Promise.resolve()秒。好处是您可以编写与普通同步函数相同的代码,但仍然让它表现得像一个承诺,因此您可以将承诺链接到它。我的这种理解不正确吗?是否有我应该注意的缺点(或好处,除了我刚才描述的)?
  3. 我使用 Promise.all() 来合并函数执行的多个独立阶段的结果。我想这很好,但是还有其他方法可以处理这种情况吗?或者这可能是一种不好的做法?相反,我是否应该链式承诺,并将承诺的结果传递给下一个,直到我达到需要使用所有承诺的水平?

try catch 块中使用 await。这是可读的,不会导致回调地狱。

async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

What is a proper way to stop execution of a block of code when an error situation arises, from a syncrhonous function, inside a promise? Moreover, like in the replacer function of String.replace, is it ok to 'throw' there? Can you suggest any better option, or maybe, a more promise-friendly approach?

扔到那里就可以了。 Promise executor 函数确实捕获了 executor 函数中发生的同步异常,并将它们变成一个被拒绝的 promise。

这是否是一件好事真的取决于你想要发生什么。如果您希望调用函数中的承诺被拒绝,那么您可以从同步函数中抛出,这将冒泡到异步调用者中的承诺并导致拒绝。否则,您也可以自由使用常规同步技术,例如 return 来自同步函数的错误条件,检查调用者中的错误,然后采取相应的行动。因此,与任何其他设计一样,它完全取决于您希望如何调用同步函数以及调用者应如何检查错误或处理错误。有时错误代码 returned 是正确的,有时抛出异常是正确的。

举个例子,如果调用 randomSyncFunctionThatMightThrow() 抛出异常,那么调用者的承诺将被自动拒绝,并且 .catch() 处理程序将被命中。

function randomSyncFunctionThatMightThrow() {
    // other logic
    throw new Error("random error");
}

someFuncThatReturnsPromise().then(arg => {
    // bunch of logic here
    randomSyncFunctionThatMightThrow();
    // other logic here
    return someThing;
}).then(result => {
    console.log(finalResult);
}).catch(err => {
    console.log(err);
});

Marking a function with async will implictly create a promise, and will also implictly convert (uncaught) throws into Promise.reject()s and returns intro Promise.resolve()s. The benefit is that you can write code identically to a normal synchronous function, but still have it behave like a promise, so you can chain promises to it. Is this understanding of mine incorrect?

我觉得不错。 async 函数有一个 try/catch 内置并自动捕获任何异常并将它们变成被拒绝的承诺。

Are there drawbacks (or benefits, other than what I just described) that I should be aware of?

强制所有同步代码以异步方式处理正常的普通 return 结果有其局限性。出于明显的编码复杂性原因,您不会将它用于整个程序中的每个同步函数。异步代码的编写和测试更耗时,因此我不会寻找方法让普通的同步事物开始 returning 承诺并让所有调用者异步处理。如果一些同步代码也处理 promises,可能偶尔会更容易与异步代码混合。但是,这不是同步代码的普通用法。

I used Promise.all() in order to combine the results of multiple, independent stages of the function execution. I suppose this is fine, but are there any other ways to deal with this situation? Or maybe it's a bad practice? Should I, instead, chain promises, and pass-through results from a promise to the next until, I reach the level where I need to use all of them?

Promise.all() 适用于您希望同时进行多个独立和并行的异步执行链的情况,您只想知道它们何时全部完成并获得所有结果立刻。 Promise chaining 适用于出于多种原因想要一个接一个地对事物进行排序的情况,但通常是因为步骤 #2 取决于步骤 #1 的结果。您应该根据异步代码的需要选择 Promise.all() 还是链接(并行执行还是顺序执行)。

将同步代码放入 Promise.all() 可以很好地工作(您甚至不必将其包装在 promise 中,因为 Promise.all() 会接受来自 运行ning 也是一个函数),但通常没有意义。同步代码是阻塞和同步的,因此将同步代码放在 Promise.all() 中通常不会获得任何并行性。我通常只是 运行 Promise.all() 之前或之后的同步代码,而不是将其与实际的异步承诺混合。

However, I am having trouble figuring out if I'm doing it the proper way.

我在下面包含了一个替代实现。作为一般做法,我不喜欢将同步代码包装在 promises 中,所以这是我的替代方案。我都使用了 async 函数,所以我可以使用 await 并且同步异常会自动转换为被拒绝的承诺,并且你的函数的合同已经 returning 了一个承诺,所以这只是实现相同结果的更简单方法(在我看来)。当然,这只是编码风格,很大程度上取决于意见。

我可能会这样编写您的代码:

async function fetchFromServerEndpoint(route, routeParams, optQueryParams) {
    const finalRoute = route.replace(/{([^{/]*?)}/g, (match, paramName) => {
        const paramValue = routeParams[paramName];

        if (paramValue === undefined) {
            // Here I need to abort execution of the replacer function
            // parent promise will be rejected
            throw new Error("UNDEFINED_ROUTE_PARAMETER");
        }
        return paramValue;
    });
    const queryParms = querystring.stringify(optQueryParams);
    const authToken = await MyAPI.getAuthorizationAsync();
    return axios.get(`${finalRoute}?${queryParams}`, {
        headers: {
            Authorization: `Basic ${authToken}`
        }
    });
}

如果你真的不喜欢await,你可以通过将末尾更改为:

来避免这种情况
    return MyAPI.getAuthorizationAsync().then(authToken => {
        return axios.get(`${finalRoute}?${queryParams}`, {
            headers: {
                Authorization: `Basic ${authToken}`
            }
        });
    });

但是,正如我在其他地方的评论中所说,我认为 await 是一个有用的工具(如果使用得当),可以让您编写更简单的代码。

  1. 终止异步块的正确方法是返回一个被拒绝的承诺或抛出一个错误,这将拒绝具有 'uncaught error' 异常的承诺,该异常有望包含堆栈跟踪

  2. async 同步 returns 一个承诺,类似于 getset 处理同步访问的方式。有一种使用 thunk 的模式(与同步访问相反)您调用 'noun' 函数来实际派生例如。 foobar() 而不是仅访问 foobar。无论如何,你可以链接承诺但输入嵌套的选项卡块,所以人们使用 await 来保持组合函数平坦

  3. Promise.all 是停止直到所有相关承诺解决的正确方法,我已经看到 Puppeteer 初始化事件并同时添加侦听器的文档,但我'我不确定线程​​是否保证顺序。

有两个主流的异步 JS (redux) 库,名为 redux-thunksagas

Redux thunk 促进声明式承诺链式操作

Sagas 使用函数生成器返回的迭代器产生的 while 循环轮询 'effects' 并允许更少的语义,但更多的动态调度。我不喜欢我们不能直接开始传奇;它们仅通过侦听 'tag along' 的其他操作来工作,例如 dispatch('MY_EVENT_START'),这些操作会立即被消耗并松散地耦合到运行的实际功能(在两个 dispatcher/listener 中注册)。通常,如果一个 Promise.all 块失败,它们的其余部分将保留 运行 (及其依赖链),但如果它已过时或不再需要等,sagas 将停止调用生成器返回的迭代器等。一个问题我们对 sagas 进行了测试,因为为了模拟一个场景,您可能需要执行 N 次才能到达那里。