Вы когда-нибудь писали асинхронный Javascript API, который использует кэширование результатов для повышения производительности при последующих вызовах? В таких сценариях часто возникает соблазн просто синхронно выполнить обратный вызов с кешированным результатом.

Давайте посмотрим, что мы имеем в виду, на примере:

const configMap = {};
function getConfig(key, callback) {
  if (configMap[key]) {
    return callback(null, configMap[key]);
  }
return makeRequest(key, (e, result) => {
    if (e) {
      return callback(e);
    }
    
    // Keep the result around for subsequent requests of this key
    configMap[key] = result;
return callback(null, result);
  });
}

Вы видите проблему? Это не страшная проблема, но при немедленном выполнении обратного вызова с закешированным результатом этот код становится синхронным.

Давайте проиллюстрируем это на примере того, как это может себя вести:

const myConfigKey = 'someKey';
getConfig(myConfigKey, (e, result) => {
  console.log(result); // Second statement to be logged.
});
console.log('First attempt.'); // First statement to be logged.
getConfig(myConfigKey, (e, result) => {
  console.log(result); // Third statement to be logged.
});
console.log('Second attempt.'); // Last statement to be logged.

Как и следовало ожидать от асинхронной функции, обратный вызов от вызова getConfig выполняется на следующем этапе цикла обработки событий после makeRequest. извлекает результат конфигурации. Но второй вызов getConfig делает что-то неожиданное.

Он вызывает наш обратный вызов синхронно.

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

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

Итак, как мы это исправим? Мы должны использовать асинхронный API, такой как setTimeout или Promise.resolve. Давайте посмотрим, как мы могли бы сделать это с помощью setTimeout.

const configMap = {};
function getConfig(key, callback) {
  if (configMap[key]) {
    return setTimeout(callback.bind(null, null, configMap[key]), 0);
  }
return makeRequest(key, (e, result) => {
    if (e) {
      return callback(e);
    }
    
    // Keep the result around for subsequent requests of this key
    configMap[key] = result;
return callback(null, result);
  });
}

Все, что мы сделали, — это обернули наш обратный вызов в setTimeout, чтобы гарантировать, что он будет выполняться только на следующем этапе цикла обработки событий. Это не только обеспечивает асинхронность кода, но и завершает стек вызовов в отношении getConfig.

Давайте посмотрим, как будет вести себя эта новая реализация:

const myConfigKey = 'someKey';
getConfig(myConfigKey, (e, result) => {
  console.log(result); // Second statement to be logged.
});
console.log('First attempt.'); // First statement to be logged.
getConfig(myConfigKey, (e, result) => {
  console.log(result); // Last statement to be logged.
});
console.log('Second attempt.'); // Third statement to be logged.

Теперь порядок записи в консоль такой, как мы и ожидали, даже если наша функция кэширует результаты из соображений производительности.

Ключевая концепция, о которой следует помнить, заключается в том, что асинхронные API должны быть осторожны, чтобы оставаться асинхронными. Рекомендуется использовать существующие асинхронные API, такие как setTimeout или Promise.resolve, чтобы убедиться, что создаваемые нами асинхронные API предсказуемы во всех случаях использования.

Автор Карлос Раймер. Первоначально опубликовано здесь.