控制流。承诺逻辑。如何在不偏离控制流的情况下偏离,基本上只留下一个 IOU(Promise)?

Control Flow. Promise Logic. How to deviate without the deviation eloping with the control flow, leaving behind basically just an IOU (Promise)?

我无法理解 JS 中异步编程的控制流。我来自经典的 OOP 背景。例如。 C++。你的程序从“主”——顶层——函数开始,它调用其他函数来做一些事情,但一切总是回到那个主函数,它保留了整体控制。每个子函数都保留对它们正在做的事情的控制,即使它们调用子函数也是如此。最终,当该主要功能结束时,程序结束。 (也就是说,这与我对 C++ 时代的记忆差不多,所以用 C++ 类比的答案可能没有帮助,哈哈)。

这使得控制流程相对容易。但是我知道这不是为了在网络服务器之类的东西上根据需要处理事件驱动编程而设计的。虽然 Javascript(让我们现在谈论节点,而不是浏览器)处理带有回调和承诺的事件驱动的 Web 服务器,相对容易......显然。

我想我终于明白了,通过事件驱动编程,应用程序的入口点可能只做一些设置一堆监听器然后离开的方式(有效地结束本身)。听众拾起所有动作并回应。

但有时东西仍然必须是同步的,这就是我一直卡住的地方。

使用回调、承诺或 async/await,我们可以有效地构建同步事件链。例如承诺:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
    console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
});

太棒了。我有一系列可以按顺序执行的任务 -- 有点像更传统的同步编程。

我的问题是:有时候需要脱链。提出一些问题并根据答案采取不同的行动。也许有条件地,您需要调用其他一些函数来获得您需要的其他东西。没有它你无法继续。但是,如果它是一个异步函数并且它给我的回报只是一个承诺呢?如何在不关闭控制流 运行 并使用该函数私奔并且永远不会返回的情况下获得实际结果?

示例:

我想在数据库中调用一个 API,获取一条记录,对该记录中的数据执行一些操作,然后将一些内容写回数据库。如果不先完成上一步,我将无法执行这些步骤中的任何一个。假设没有任何同步函数可以处理这个 API。没问题。 Promise 链(如上)似乎是一个很好的解决方案。

但是......假设我第一次调用数据库时,我之前为它获取的授权令牌已经过期,我必须获得一个新的。我不知道,直到我打了第一个电话。我不想每次都获得(甚至测试是否需要)一个新的身份验证令牌。我只是想在呼叫失败时能够响应,因为我需要一个。

好的...在可能看起来像这样的同步伪代码中:

let Token = X
Step 1: Call the database(Token).  Wait for the response.
Step 2: If response says need new token, then:
    Token = syncFunctionThatGetsAndReturnsNewToken(). 
        // here the program waits till that function is done and I've got my token.
    Repeat Step 1
End if
Step 3: Do the rest of what I need to do.

但是现在我们需要在 Javascript/node 中只使用异步函数来完成,所以我们可以使用承诺(或回调)链?

let Token = X
CallDatabase(Token)
.then(check if response says we need new token, and if so, get one)
.then(... 

等一下。那个“如果是这样,得到一个”是搞砸我的部分。 JS/node 中的所有这些异步性不会等待它。该功能只是在将来某个时候“承诺”给我一个新令牌。这是一张欠条。伟大的。不能用借据调用数据库。好吧,我很乐意等待,但节点和 JS 不允许我这样做,因为那会阻塞。

简而言之(嗯,好吧,相当大)。我错过了什么?我如何使用回调或 Promises 执行上述操作?

我敢肯定,在不久的将来,这里会有一个愚蠢的“呃”时刻,这要感谢你们中的一个或多个好人。我很期待。提前致谢!

您对 .then 调用所做的是附加一个函数,当 Promise 在未来 任务 中解析时,该函数将 运行 执行。该函数的处理本身是同步的,并且可以使用您想要的所有控制流:

 getResponse()
.then(response => {
   if(response.needsToken) 
      return getNewToken().then(getResponse);
})
.then(() => /* either runs if token is not expired or token was renewed */)

如果令牌已过期,则不会直接调度 .then 返回的 Promise,而是启动一个新的异步操作来检索新令牌。如果该异步操作已完成,在新任务中它将解析 Promise it returns,并且由于该 Promise 是从 .then 回调返回的,因此这也将解析外部 Promise 和 Promise链条继续。

请注意,这些 Promise 链可能会很快变得复杂,并且使用 async function 可以将其编写得更优雅(尽管在引擎盖下它大致相同):

 do {
   response = await getResponse();
   if(response.needsToken)
      await renewToken();
 } while(response.needsToken)

首先,我建议不要使用 then 和 catch 方法来监听 Promise 结果。他们倾向于创建难以阅读和维护的过于嵌套的代码。

我为你的案例制作了一个原型,它使用了 async/await。它还具有一种机制来跟踪我们对数据库进行身份验证的尝试。如果我们达到最大尝试次数,则可以向管理员等发送紧急警报以进行通知。这避免了尝试身份验证的无限循环,而是帮助您采取适当的行动。

'use strict'

var token;

async function getBooks() {
    // In case you are not using an ORM(Sequelize, TypeORM), I would suggest to use
    // at least a query builder like Knex
    const query = generateQuery(options);
    const books = executeQuery(query)
}

async function executeQuery(query) {
    let attempts = 0;
    let authError = true;

    if (!token) {
        await getDbAuthToken();
    }

    while (attemps < maxAttemps) {
        try {
            attempts++;
            // call database
            // return result
        }
        catch(err) {
            // token expired
            if (err.code == 401) {
                await getDbAuthToken();
            }
            else {
                authError = false;
            }
        }
    }

    throw new Error('Crital error! After several attempts, authentication to db failed. Take immediate steps to fix this')
}

// This can be sync or async depending on the flow 
// how the auth token is retrieved
async function getDbAuthToken() {

}