JavaScript - 将上下文附加到异步函数调用?

Dav*_*nan 14 javascript method-call async-await asynchronous-method-call

同步函数调用上下文

在 JavaScript 中,通过在全局范围内使用堆栈,可以轻松地将某些上下文与同步函数调用相关联。

// Context management

let contextStack = [];
let context;

const withContext = (ctx, func) => {
  contextStack.push(ctx);
  context = ctx;

  try {
    return func();
  } finally {
    context = contextStack.pop();
  }
};

// Example

const foo = (message) => {
  console.log(message);
  console.log(context);
};

const bar = () => {
  withContext("calling from bar", () => foo("hello"));
};

bar();
Run Code Online (Sandbox Code Playgroud)

这允许我们编写特定于上下文的代码,而不必到处传递上下文对象,并且我们使用的每个函数都依赖于该上下文对象。

这在 JavaScript 中是可能的,因为保证了顺序代码执行,也就是说,这些同步函数在任何其他代码可以修改全局状态之前运行完成。

生成器函数调用上下文

我们可以使用生成器函数实现类似的功能。生成器函数使我们有机会在生成器函数的概念执行恢复之前进行控制。这意味着,即使执行暂停几秒钟(即,该函数在任何其他代码运行之前都没有运行完成),我们仍然可以确保其执行附加了准确的上下文。

const iterWithContext = function* (ctx, generator) {
  // not a perfect implementation

  let iter = generator();
  let reply;

  while (true) {
    const { done, value } = withContext(ctx, () => iter.next(reply));
    
    if (done) {
      return;
    }
    
    reply = yield value;
  }
};
Run Code Online (Sandbox Code Playgroud)

问题:异步函数调用上下文?

将一些上下文附加到异步函数的执行也非常有用。

const timeout = (ms) => new Promise(res => setTimeout(res, ms));

const foo = async () => {
  await timeout(1000);
  console.log(context);
};

const bar = async () => {
  await asyncWithContext("calling from bar", foo);
};
Run Code Online (Sandbox Code Playgroud)

问题是,据我所知,没有办法拦截异步函数恢复执行之前的时刻或异步函数挂起执行之后的时刻,以提供此上下文。

有什么方法可以实现这一点吗?

我现在最好的选择是不使用异步函数,而是使用行为类似于异步函数的生成器函数。但这不太实用,因为它需要整个代码库都这样编写。

背景/动机

使用这样的上下文非常有价值,因为上下文在调用堆栈深处可用。如果库需要调用外部处理程序,这样如果处理程序回调到库,则库将具有适当的上下文,这尤其有用。例如,我想象 React hooks 和 Solid.js 在底层以这种方式广泛使用上下文。如果不这样做,程序员将不得不在各处传递上下文对象,并在回调库时使用它,这既混乱又容易出错。上下文是一种根据我们在调用堆栈中的位置,从函数调用中巧妙地“柯里化”或抽象出上下文对象的方法。这是否是一个好的实践还有待商榷,但我认为我们可以同意这是图书馆作者选择做的事情。我想将上下文的使用扩展到异步函数,从概念上讲,异步函数在执行流程方面的行为应该类似于同步函数。

赏金

我现在才意识到以前接受的答案在浏览器中不起作用,因为浏览器不实现异步堆栈跟踪。我希望有可能进行替代黑客攻击,因此我开始了另一笔赏金。

Wol*_*DEV 6

2023年2月23日更新

现在有一个简单的 NPM 模块用于提供同步和异步函数上下文:
https ://www.npmjs.com/package/function-contexts

原答案

据我所知,ECMA 没有对“上下文”的规范(无论它是普通函数还是异步函数)。因此,您针对正常功能发布的解决方案已经是一个 hack。

根据 ECMA 标准,没有基于 JavaScript 的 API 可以挂钩 wait 来执行类似生成器的技巧。所以你必须依赖(基于环境的)黑客。这些技巧可能很大程度上取决于您所使用的环境。

仅 JavaScript(需要异步堆栈跟踪)

以下是一种纯粹基于异步堆栈跟踪的解决方案。
由于几乎所有 JavaScript 解释器都基于 V8,因此几乎适用于所有用例。

const kContextIdFunctionPrefix = "__context_id__";
const kContextIdRegex = new RegExp(`${kContextIdFunctionPrefix}([0-9]+)`);
let contextIdOffset = 0;

function runWithContextId(target, ...args) {
    const contextId = ++contextIdOffset;
    let proxy;
    eval(`proxy = async function ${kContextIdFunctionPrefix}${contextId}(target, ...args){ return await target.call(this, ...args); }`);
    return proxy.call(this, target, ...args);
}

function getContextId() {
    const stack = new Error().stack.split("\n");
    for(const frame of stack) {
        const match = frame.match(kContextIdRegex);
        if(!match) {
            continue;
        }

        const id = parseInt(match[1]);
        if(isNaN(id)) {
            console.warn(`Context id regex matched, but failed to parse context id from ${match[1]}`);
            continue;
        }

        return id;
    }

    console.log(new Error().stack)
    throw new Error("getContextId() called without providing a context (runWithContextId(...))");
}
Run Code Online (Sandbox Code Playgroud)

一个简单的演示:

async function main() {
    const target = async () => {
        const contextId = getContextId();
        console.log(`Context Id: ${contextId}`);
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log(`Context Id (After await): ${getContextId()} (before: ${contextId})`);

        return contextId;
    };

    const contextIdA = runWithContextId(target);
    const contextIdB = runWithContextId(target);

    // Note: We're first awaiting the second call!
    console.log(`Invoke #2 context id: ${await contextIdB}`);
    console.log(`Invoke #1 context id: ${await contextIdA}`);
}
main();
Run Code Online (Sandbox Code Playgroud)

该解决方案利用堆栈跟踪来识别上下文 ID。遍历(同步和异步)堆栈跟踪并使用具有特殊名称的动态生成的函数允许传递特殊值(在本例中为数字)。

NodeJS(异步本地存储)

NodeJS 提供了一种异步上下文跟踪的方法: https://nodejs.org/api/async_context.html#class-asynclocalstorage
应该可以使用 AsyncLocalStorage 构建异步上下文。

使用转译器

您可能想使用转译器(如 babel 或 typescript)将异步函数动态转换为生成器函数。使用转译器甚至可以让您编写一个插件来实现基于生成器函数的异步上下文。