Статьи

Упрощение асинхронного кодирования с помощью асинхронных функций

Песочные часы с песком, льющимся через

Дебют Promises в JavaScript зажег Интернет в огне — они помогают разработчикам вырваться из ада обратного вызова и решить множество проблем, которые преследуют асинхронный код программистов JavaScript повсюду. Однако обещания далеко не безупречны. Они все еще требуют обратных вызовов, могут все еще быть грязными в сложных ситуациях и невероятно многословны.

22 марта 2017 года . Эта статья была обновлена ​​с учетом изменений в спецификации и текущей поддержки времени выполнения.

С появлением ES6 (далее — ES2015), который не только давал обещания, родные для языка, не требуя одной из бесчисленных доступных библиотек, мы также получили генераторы . Генераторы имеют возможность приостанавливать выполнение внутри функции, что означает, что, помещая их в служебную функцию , мы можем дождаться завершения асинхронной операции, прежде чем перейти к следующей строке кода. Внезапно ваш асинхронный код может начать выглядеть синхронно!

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

Для запуска примеров кода async / await из этой статьи вам потребуется совместимый браузер.

Совместимость во время выполнения

На стороне клиента Chrome, Firefox и Opera теперь поддерживают асинхронные функции из коробки.

Начиная с версии 7.6, Node.js также поставляется с включенным по умолчанию async / await.

Асинхронные функции против генераторов

Вот пример использования генераторов для асинхронного программирования. Он использует библиотеку Q :

var doAsyncOp = Q.async(function* () {
  var val = yield asynchronousOperation();
  console.log(val);
  return val;
});

Q.async Символ *yield Q.asyncdoAsyncOpasync function doAsyncOp () {
var val = await asynchronousOperation();
console.log(val);
return val;
};

Вот как это выглядит, когда вы избавляетесь от лишнего, используя новый синтаксис, включенный в ES7:

 async

Это не сильно отличается, но мы удалили функцию-обертку и звездочку и заменили их на ключевое слово yield Ключевое слово awaitasynchronousOperation Эти два примера сделают одно и то же: дождитесь завершения valfunction doAsyncOp () {
return asynchronousOperation().then(function(val) {
console.log(val);
return val;
});
};

Преобразование обещаний в асинхронные функции

Как бы выглядел предыдущий пример, если бы мы использовали ванильные обещания?

 then

В нем столько же строк, но из-за этого имеется много дополнительного кода, и функция обратного вызова передана ему. Другая неприятность — дублирование ключевого слова return Это всегда было чем-то, что мешало мне, потому что это затрудняет точное определение того, что возвращается из функции, которая использует обещания.

Как видите, эта функция возвращает обещание, которое будет соответствовать значению val И угадайте, что … так делают примеры генератора и асинхронной функции! Всякий раз, когда вы возвращаете значение из одной из этих функций, вы фактически неявно возвращаете обещание, которое разрешается до этого значения. Если вы вообще ничего не возвращаете, вы неявно возвращаете обещание, которое становится undefined

Цепные операции

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

Вот как вы могли бы связывать асинхронные операции, используя обещания (по общему признанию, мы глупы и просто выполняем одно и то же asynchronousOperation

 function doAsyncOp() {
  return asynchronousOperation()
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    });
}

С асинхронными функциями мы можем действовать так, как будто asynchronousOperation

 async function doAsyncOp () {
  var val = await asynchronousOperation();
  val = await asynchronousOperation(val);
  val = await asynchronousOperation(val);
  return await asynchronousOperation(val);
};

Вам даже не нужно ключевое слово await

Параллельные операции

Еще одна замечательная особенность обещаний — возможность выполнять несколько асинхронных операций одновременно и продолжать выполнение, как только все они будут выполнены. Promise.all() — способ сделать это в соответствии со спецификацией ES2015.

Вот пример:

 function doAsyncOp() {
  return Promise.all([
    asynchronousOperation(),
    asynchronousOperation()
  ]).then(function(vals) {
    vals.forEach(console.log);
    return vals;
  });
}

Это также возможно с асинхронными функциями, хотя вам все еще нужно использовать Promise.all()

 async function doAsyncOp() {
  var vals = await Promise.all([
    asynchronousOperation(),
    asynchronousOperation()
  ]);
  vals.forEach(console.log.bind(console));
  return vals;
}

Это все еще намного чище, даже с Promise.all

Обработка отклонения

Обещания могут быть разрешены или отклонены. Отклоненные обещания могут быть обработаны второй функцией, переданной в thencatch Поскольку мы не используем какие-либо методы API Promise Мы делаем это с trycatch При использовании асинхронных функций отклонения передаются как ошибки, и это позволяет обрабатывать их с помощью встроенного кода обработки ошибок JavaScript.

 function doAsyncOp() {
  return asynchronousOperation()
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .catch(function(err) {
      console.error(err);
    });
}

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

 async function doAsyncOp () {
  try {
    var val = await asynchronousOperation();
    val = await asynchronousOperation(val);
    return await asynchronousOperation(val);
  } catch (err) {
    console.err(err);
  }
};

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

Нарушенные обещания

Чтобы отклонить собственное обещание, вы можете использовать rejectPromisePromisethencatch Если ошибка выходит за пределы этой области, она не будет содержаться в обещании.

Вот несколько примеров способов отклонения обещаний:

 function doAsyncOp() {
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      reject("something is bad");
    }
    resolve("nothing is bad");
  });
}

/*-- or --*/

function doAsyncOp() {
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      reject(new Error("something is bad"));
    }
    resolve("nothing is bad");
  });
}

/*-- or --*/

function doAsyncOp() {
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      throw new Error("something is bad");
    }
    resolve("nothing is bad");
  });
}

Как правило, лучше всего использовать new Error

Вот несколько примеров, когда обещание не поймает ошибку:

 function doAsyncOp() {
  // the next line will kill execution
  throw new Error("something is bad");
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      throw new Error("something is bad");
    }
    resolve("nothing is bad");
  });
}

// assume `doAsyncOp` does not have the killing error
function x() {
  var val = doAsyncOp().then(function() {
    // this one will work just fine
    throw new Error("I just think an error should be here");
  });
  // this one will kill execution
  throw new Error("The more errors, the merrier");
  return val;
}

При использовании асинхронных функций обещания отклоняются из-за ошибок. Проблема с областью не возникает — вы можете выдать ошибку в любом месте асинхронной функции, и она будет поймана обещанием:

 async function doAsyncOp() {
  // the next line is fine
  throw new Error("something is bad");
  if (somethingIsBad) {
    // this one is good too
    throw new Error("something is bad");
  }
  return "nothing is bad";
} 

// assume `doAsyncOp` does not have the killing error
async function x() {
  var val = await doAsyncOp();
  // this one will work just fine
  throw new Error("I just think an error should be here");
  return val;
}

Конечно, мы никогда не вернемся к этой второй ошибке или к returndoAsyncOp

Gotchas

Если вы новичок в асинхронных функциях, нужно знать, что нужно использовать вложенные функции. Например, если у вас есть другая функция в вашей асинхронной функции (обычно как обратный вызов чего-либо), вы можете подумать, что вы можете просто использовать await Ты не можешь Вы можете использовать awaitasync

Например, это не работает:

 async function getAllFiles(fileNames) {
  return Promise.all(
    fileNames.map(function(fileName) {
      var file = await getFileAsync(fileName);
      return parse(file);
    })
  );
}

await Вместо этого к функции обратного вызова должно быть прикреплено ключевое слово async

 async function getAllFiles(fileNames) {
  return Promise.all(
    fileNames.map(async function(fileName) {
      var file = await getFileAsync(fileName);
      return parse(file);
    })
  );
}

Это очевидно, когда вы видите это, но, тем не менее, это то, что вам нужно остерегаться.

Если вам интересно, вот эквивалент использования обещаний:

 function getAllFiles(fileNames) {
  return Promise.all(
    fileNames.map(function(fileName) {
      return getFileAsync(fileName).then(function(file) {
        return parse(file);
      });
    })
  );
}

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

 var a = doAsyncOp(); // one of the working ones from earlier
console.log(a);
a.then(function() {
  console.log("`a` finished");
});
console.log("hello");

/* -- will output -- */
Promise Object
hello
`a` finished

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

Лучший асинхронный код сегодня!

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

Существует несколько инструментов, которые могут компилировать асинхронные функции (и другие функции ES2015 +) вплоть до кода ES5. Если вы используете Babel , это просто установка предустановки ES2017 .

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