JavaScript - прикрепить контекст к вызову асинхронной функции?

Синхронный контекст вызова функции

В 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.info(message);
  console.info(context);
};

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

bar();

Это позволяет нам писать зависящий от контекста код без необходимости передавать объект контекста повсюду, и каждая используемая функция зависит от этого объекта контекста.

Это возможно в 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;
  }
};

Вопрос: Контекст вызова асинхронной функции?

Также было бы очень полезно привязать некоторый контекст к выполнению асинхронной функции.

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

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

const bar = async () => {
  await asyncWithContext("calling from bar", foo);
};

Проблема в том, что, насколько мне известно, нет способа перехватить момент перед тем, как асинхронная функция возобновит выполнение, или момент после того, как асинхронная функция приостановит выполнение, чтобы обеспечить этот контекст.

Есть ли способ достичь этого?

Мой лучший вариант сейчас — не использовать асинхронные функции, а использовать функции-генераторы, которые ведут себя как асинхронные функции. Но это не очень практично, поскольку требует, чтобы вся кодовая база была написана таким образом.

Фон/мотивация

Использование такого контекста невероятно ценно, потому что контекст доступен глубоко в стеке вызовов. Это особенно полезно, если библиотеке необходимо вызвать внешний обработчик, чтобы, если обработчик снова вызывает библиотеку, у библиотеки был соответствующий контекст. Например, я полагаю, что хуки React и Solid.js широко используют контекст таким образом, неявно. Если бы это было не так, программисту пришлось бы повсюду передавать объект контекста и использовать его при обратном обращении к библиотеке, что и беспорядочно, и подвержено ошибкам. Контекст — это способ аккуратно «каррировать» или абстрагировать объект контекста от вызовов функций в зависимости от того, где мы находимся в стеке вызовов. Является ли это хорошей практикой или нет, спорный вопрос, но я думаю, что мы можем согласиться с тем, что авторы библиотек решили это сделать. Я хотел бы расширить использование контекста на асинхронные функции, которые должны концептуально вести себя как синхронные функции, когда дело доходит до потока выполнения.

Можете ли вы рассказать о преимуществах этого по сравнению с использованием .call() или .apply() для обеспечения привязки this для вызываемой функции?

Pointy 12.02.2023 16:05

Или .bind() предоставить одну или несколько уже связанных версий функции?

Pointy 12.02.2023 16:06

@Pointy Контекст доступен глубоко в стеке вызовов. Это особенно полезно, если библиотеке необходимо вызвать внешний обработчик, чтобы, если обработчик снова вызывает библиотеку, у библиотеки был соответствующий контекст. Например, я думаю, что хуки React и Solid.js широко используют контекст таким образом.

David Callanan 12.02.2023 16:18

Кстати, в качестве альтернативы для создания хака для контекстов вы можете вместо этого использовать внедрение зависимостей.

WolverinDEV 15.02.2023 17:06

@WolverinDEV Да, абсолютно. Но если одна часть вашей кодовой базы не выполняет DI (что очень часто), то это все портит. И даже с DI вы не можете писать код на основе контекста стека вызовов, что, возможно, все еще важно в некоторых ситуациях. С DI вы можете использовать closures/similar для настройки определенных реализаций в рамках своего рода архитектурной области, но не в пределах области вызова функции времени выполнения. Кажется, это два разных типа областей, в которых вы хотели бы предоставить конкретные реализации, и этот вопрос касается последнего.

David Callanan 16.02.2023 02:41

@WolverinDEV Я считаю, что во многих случаях разработчики заботятся об области вызова функции, когда они на самом деле должны беспокоиться об архитектурной области, но я все же думаю, что бывают редкие ситуации, когда область вызова функции имеет значение, на что и направлен этот вопрос. адрес.

David Callanan 16.02.2023 02:48

Вы имеете в виду: const withContext = (ctx, func) => { с =>. PS: Работающий фрагмент был бы полезен, чтобы испытать код на месте.

Roko C. Buljan 20.02.2023 21:17

@RokoC.Buljan Спасибо. Первый фрагмент теперь доступен для запуска.

David Callanan 21.02.2023 15:08
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
6
8
221
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Насколько мне известно, в ECMA нет спецификации для "контекстов" (независимо от того, обычная это функция или асинхронная). Поэтому решение, которое вы разместили для обычных функций, уже является взломом.

Согласно стандарту ECMA, не существует API на основе JavaScript, который мог бы перехватывать await, чтобы выполнять трюк, подобный генератору. Таким образом, вы должны полагаться на (основанные на среде) хаки. Эти хаки могут сильно зависеть от среды, которую вы используете.

Только 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.info(new Error().stack)
    throw new Error("getContextId() called without providing a context (runWithContextId(...))");
}

Простая демонстрация:

async function main() {
    const target = async () => {
        const contextId = getContextId();
        console.info(`Context Id: ${contextId}`);
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.info(`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.info(`Invoke #2 context id: ${await contextIdB}`);
    console.info(`Invoke #1 context id: ${await contextIdA}`);
}
main();

Это решение использует трассировку стека для определения идентификатора контекста. Обход трассировки стека (синхронной и асинхронной) и использование динамически генерируемых функций со специальными именами позволяет передать специальное значение (в данном случае число).

NodeJS (асинхронное хранилище)

NodeJS предлагает способ асинхронного отслеживания контекста: https://nodejs.org/api/async_context.html#class-asynclocalstorage
Должна быть возможность создать асинхронный контекст с помощью AsyncLocalStorage.

Использование транспилятора

Возможно, вы захотите использовать транспилятор (например, babel или typescript), который на лету преобразует асинхронные функции в функции генератора. Использование транспилятора позволяет даже написать плагин для реализации асинхронных контекстов на основе функций генератора.

Ха-ха, это хороший хак с трассировкой стека. Наверное, тоже не слишком медленно.

David Callanan 15.02.2023 16:17

может связать? После привязки у вас есть новый экземпляр функции с захваченным «этим» (в моем случае это ноль) или захваченными входными параметрами.

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


const foo = async function (context) {
  await timeout(1000);
  console.info(context);
};
const fooOuter = foo.bind(null, "calling from outer");

const bar = async () => {
  await fooOuter();
  await foo("calling from bar");
};

Это не соответствует моим требованиям к написанию «контекстно-зависимого кода без необходимости передавать объект контекста повсюду и иметь каждую используемую нами функцию, зависящую от этого объекта контекста» и иметь контекст «доступный глубоко в стеке вызовов» таким образом это не "подвержено ошибкам".

David Callanan 21.02.2023 15:11

Другие вопросы по теме