Создание Promise
— это самый простой способ убедиться, что значение будет заключено в Promise
. Если значение уже является Promise
, то оно просто возвращается. В противном случае создаётся новый Promise
, который сразу же завершается с переданным значением.
let fifteen = Promise.resolve(15);
fifteen.then(value => console.log(`Got ${value}`));
// → Got 15
Для получения результата Promise
можно использовать его метод then
. Он регистрирует функцию обратного вызова, которая вызывается при разрешении Promise
. Можно добавить несколько обратных вызовов к одному Promise
, и они будут вызваны после разрешения Promise
.
Но это не всё, что делает метод then
. Он возвращает новый Promise
, который разрешает значение, возвращённое функцией обработки, или ожидает другой Promise
и разрешает его значение.
Можно рассматривать Promise
как средство преобразования значения в асинхронное. Обычное значение существует здесь и сейчас, а обещанное значение может появиться в будущем. Вычисления, основанные на Promise
, выполняются асинхронно и срабатывают, когда значение становится доступным.
Чтобы создать Promise
, можно использовать Promise
в качестве конструктора. У него есть немного странный интерфейс: конструктор принимает функцию в качестве аргумента и немедленно вызывает её, передавая другую функцию для разрешения этого Promise
. Это работает таким образом, чтобы только код, создающий Promise
, мог его разрешить.
Вот пример создания интерфейса на основе Promise
для функции readStorage
:
function storage(nest, name) {
return new Promise(resolve => {
nest.readStorage(name, result => resolve(result));
});
}
storage(bigOak, "enemies")
.then(value => console.log("Got", value));
Эта асинхронная функция возвращает значимое значение. Это одно из главных преимуществ Promise
: они упрощают использование асинхронных функций. Функции, основанные на Promise
, не требуют передачи обратных вызовов, а работают подобно обычным функциям: они принимают входные данные в качестве параметров и возвращают выходные данные. Единственное отличие заключается в том, что выходные данные могут быть ещё недоступны.
В обычном JavaScript вычисления могут завершиться неудачно из-за выброса исключения. Асинхронные вычисления также часто требуют чего-то подобного. Например, сетевой запрос может завершиться ошибкой, или какой-либо код, являющийся частью асинхронного вычисления, может вызвать исключение.
Одной из самых насущных проблем асинхронного программирования в стиле обратных вызовов является обеспечение правильного сообщения об ошибке обратному вызову.
Широко используемое соглашение заключается в том, что первый параметр обратного вызова используется для указания на сбой операции, а второй параметр содержит значение, которое было бы сгенерировано в случае успешного выполнения операции. Эта функция обратного вызова должна всегда проверять, получила ли она ошибку, и гарантировать, что любая проблема, включая исключения, вызванные функциями, которые она вызывает, будет перехвачена и передана правильной функции.
Promise
упрощает эту задачу. Они могут разрешаться (операция успешно завершена) или отклоняться (сбой). Только когда операция успешно завершается, вызывается обработчик разрешения (then
), и отклонение автоматически распространяется на новый Promise
, возвращаемый then
. Когда обработчик генерирует исключение, это автоматически приводит к отклонению Promise
, созданного этим обработчиком. Таким образом, если любой элемент в цепочке асинхронных операций терпит неудачу, весь результат цепочки помечается как отказ, и никакие последующие обычные обработчики после отказавшей позиции не вызываются.
Подобно тому, как разрешение Promise
предоставляет значение, отказ также предоставляет значение, обычно называемое причиной отказа. Когда исключение в обработчике приводит к отказу, исключение становится причиной отказа. Аналогично, когда обработчик возвращает отклонённое Promise
, отказ переходит к следующему Promise
. Функция Promise.reject
создаёт новое, сразу отклонённое Promise
.
Чтобы явно обрабатывать отказы, Promise
имеет метод catch
, который регистрирует обработчик, вызываемый при отклонении Promise
, аналогично обработчику разрешения. Это также очень похоже на then
, поскольку он возвращает новый Promise
, который разрешается исходным значением Promise
, если оно разрешено, или результатом обработчика catch
в противном случае. Если обработчик catch
генерирует ошибку, новый Promise
также отклоняется.
Как сокращение, then
также принимает обработчик отказа в качестве второго параметра, поэтому можно настроить оба обработчика в одном методе вызова.
Функция, переданная конструктору Promise
, получает второй параметр, который можно использовать вместе с функцией разрешения для отклонения нового Promise
.
Цепочка значений Promise
, созданная вызовами then
и catch
, может рассматриваться как поток асинхронных значений или отказов, проходящих через конвейер. Поскольку цепочка создаётся путём регистрации обработчиков, каждый конвейер имеет успешный обработчик или связанный с ним обработчик отказа (или оба). Обработчики, не соответствующие типу результата (успех или отказ), игнорируются. Однако те, которые соответствуют, вызываются, и их результат определяет, какое значение появится следующим — успешное возвращение не-Promise
значения, отказ при выбросе исключения и разрешение с использованием возвращённого Promise
или его значения.
Аналогично тому, как среда обрабатывает неперехваченные исключения, JavaScript-среда может обнаруживать необработанные отказы Promise
и сообщать о них как об ошибках.
Иногда система зеркал ворона не получает достаточно света для передачи сигнала или что-то блокирует путь сигнала. Сигнал может быть отправлен, но никогда не получен.
Фактически, это просто приведёт к тому, что предоставленный обратный вызов для send
никогда не будет вызван, что может привести к остановке программы без уведомления о проблеме. Если запрос не получит ответа в течение определённого периода времени, он истечёт по таймауту и сообщит об ошибке.
Обычно проблемы с передачей являются случайными событиями, такими как свет фар автомобиля, мешающий световому сигналу, и требуется лишь повторная попытка запроса, чтобы сделать его успешным. Поэтому, когда мы обрабатываем их, мы позволяем нашим функциям запросов автоматически повторять попытку отправки запроса несколько раз перед тем, как сдаться.
Кроме того, поскольку мы уже определили, что Promise
— это хорошо, мы также сделаем так, чтобы наши функции запросов возвращали Promise
. Для содержания, которое они могут выразить, обратные вызовы и Promise
эквивалентны. Основанные на обратных вызовах функции могут быть упакованы для предоставления основанного на Promise
интерфейса, и наоборот.
Даже если запрос и ответ успешно переданы, ответ также может указывать на сбой — например, если запрос пытается использовать неопределённый тип запроса или обработчик, возникает ошибка. Чтобы поддержать это, send
и defineRequestType
следуют упомянутому выше соглашению, где первый переданный обратному вызову параметр является причиной сбоя, если таковая имеется, а вторым параметром является фактическое значение.
Эти могут быть преобразованы нашими упаковщиками в разрешение или отклонение Promise
.
class Timeout extends Error {}
function request(nest, target, type, content) {
return new Promise((resolve, reject) => {
let done = false;
function attempt(n) {
nest.send(target, type, content, (failed, value) => {
done = true;
if (failed) reject(failed);
else resolve(value);
});
setTimeout(() => {
if (done) return;
else if (n < 3) attempt(n + 1);
else reject(new Timeout("Timed out"));
}, 250);
}
attempt(1);
});
}
Поскольку Promise
может разрешиться (или отклониться) только один раз, это эффективно. Первый вызов resolve
или reject
определяет результат Promise
, и любые последующие вызовы (например, завершение запроса после достижения тайм-аута или возврат другого запроса после завершения другого запроса) будут проигнорированы.
Нам нужно использовать рекурсивную функцию для построения асинхронного цикла для повторных попыток, поскольку обычные циклы не позволяют нам останавливаться и ждать асинхронной операции. Функция attempt
пытается отправить запрос один раз. Она также устанавливает тайм-аут, и если нет ответа через 250 миллисекунд, начинается следующая попытка, или, если это четвёртая попытка, она отклоняет этот Promise
с помощью экземпляра Timeout
.
Каждая четверть секунды предпринимается попытка повторного запроса, и через секунду после отсутствия ответа запрос отменяется, что является произвольным. Возможно, даже если запрос действительно пришёл, но обработчик занял больше времени, запрос будет отправлен повторно. Мы напишем наш обработчик и запомним эту проблему — повторяющиеся сообщения должны быть безвредными.
В общем, мы не строим здесь мир, полный мощных сетевых возможностей. Но это нормально — в вычислительном отношении у ворона нет высоких ожиданий.
Мы продолжим и создадим оболочку для defineRequestType
, которая позволяет обработчикам возвращать Promise
или явное значение и соединяется с нашим обратным вызовом. ```
response),
failure => callback(failure));
} catch (exception) {
callback(exception);
}
});
}
Если возвращаемое значение обработчика не является Promise, для преобразования его в Promise используется Promise.resolve.
Обратите внимание, что вызов обработчика должен быть заключён в блок try, чтобы гарантировать, что любое непосредственно вызванное исключение будет передано функции обратного вызова. Это хорошо иллюстрирует сложность правильного обращения с ошибками с помощью исходных обратных вызовов — легко забыть правильно обработать подобное исключение, и в противном случае ошибка не будет сообщена правильному обратному вызову. Promise делает это в значительной степени автоматическим и, следовательно, менее подверженным ошибкам.
## Набор Promise
Каждый компьютер в гнезде сохраняет в своём свойстве neighbors массив других гнёзд в пределах расстояния передачи. Чтобы проверить, какие из них доступны, вы можете написать функцию, которая пытается отправить каждому гнезду запрос «ping» (простой запрос на ответный отклик), и посмотреть, какие ответы возвращаются.
При работе с набором одновременно выполняемых Promise функция Promise.all может оказаться полезной. Она возвращает Promise, который ожидает разрешения всех Promise в массиве, а затем разрешает эти значения Promise в массив (в том же порядке, что и исходный массив). Если какой-либо Promise отклоняется, результат Promise.all также отклоняется.
```js
requestType("ping", () => "pong");
function availableNeighbors(nest) {
let requests = nest.neighbors.map(neighbor => {
return request(nest, neighbor, "ping")
.then(() => true, () => false);
});
return Promise.all(requests).then(result => {
return nest.neighbors.filter((_, i) => result[i]);
});
}
Когда сосед недоступен, мы не хотим, чтобы весь составной Promise потерпел неудачу, потому что тогда мы всё ещё не знаем ничего определённого. Поэтому мы отображаем функцию на массив соседей, превращая их в запросы Promise и добавляя обработчики, которые создают true для успешных запросов и false для отклонённых.
В обработчике составного Promise фильтр используется для удаления элементов из массива neighbors, соответствующих значению false. Это использует тот факт, что filter передаёт текущий элемент массива в качестве второго параметра своей функции фильтрации (map, some и аналогичные высокоуровневые методы массива делают то же самое).
Тот факт, что гнёзда могут общаться только со своими соседями, значительно снижает полезность этой сети.
Чтобы транслировать информацию по всей сети, одним из решений является настройка автоматического переадресации запроса соседям. Затем эти соседи переадресуют запрос своим соседям, пока вся сеть не получит сообщение.
import {everywhere} from "./crow-tech";
everywhere(nest => {
nest.state.gossip = [];
});
function sendGossip(nest, message, exceptFor = null) {
nest.state.gossip.push(message);
for (let neighbor of nest.neighbors) {
if (neighbor == exceptFor) continue;
request(nest, neighbor, "gossip", message);
}
}
requestType("gossip", (nest, message, source) => {
if (nest.state.gossip.includes(message)) return;
console.log(`${nest.name} received gossip '${
message}' from ${source}`);
sendGossip(nest, message, source);
});
Чтобы избежать бесконечной отправки одного и того же сообщения по сети, каждое гнездо сохраняет набор уже увиденных сообщений сплетен. Для определения этого массива мы используем функцию everywhere (которая запускает код на каждом гнезде) для добавления свойства к объекту состояния гнезда, где мы будем хранить локальное состояние гнезда.
Когда гнездо получает повторяющееся сообщение сплетен, оно игнорирует его. Это вполне вероятно при слепой повторной отправке этих сообщений всеми. Однако когда он получает новое сообщение, он взволнованно сообщает об этом всем своим соседям, кроме отправителя сообщения.
Это приведёт к распространению новой сплетни по сети, как чернила в воде. Даже если некоторые соединения в настоящее время не работают, если есть альтернативный маршрут к указанному гнезду, сплетня достигнет его через этот маршрут.
Этот способ общения в сети называется паводком — он заполняет всю сеть информацией, пока все узлы не получат её.
Мы можем вызвать sendGossip, чтобы увидеть поток сообщений в деревне.
sendGossip(bigOak, "Kids with airgun in the park");
Если заданный узел хочет связаться с другим отдельным узлом, паводок не очень эффективный метод. Особенно в больших сетях это приводит к большому количеству ненужного трафика.
Другой подход заключается в настройке маршрутизации сообщений от узла к узлу до тех пор, пока они не достигнут места назначения. Сложность здесь заключается в том, что для этого требуется знание сетевой топологии. Чтобы отправить запрос в далёкое гнездо, необходимо знать, какое из ближайших гнёзд находится ближе всего к месту назначения. Отправка его в неправильном направлении не принесёт большой пользы.
Поскольку каждое гнездо знает только своих непосредственных соседей, у него нет информации, необходимой для расчёта маршрутов. Мы должны каким-то образом передать эту информацию о соединениях всем гнёздам. Когда гнездо заброшено или построено новое, лучше всего разрешить ему изменяться со временем.
Мы снова можем использовать паводок, но вместо проверки того, было ли уже получено данное сообщение, мы проверяем, соответствует ли новый набор соседей для данного гнезда тому, который у нас уже есть.
requestType("connections", (nest, {name, neighbors},
source) => {
let connections = nest.state.connections;
if (JSON.stringify(connections.get(name)) ==
JSON.stringify(neighbors)) return;
connections.set(name, neighbors);
broadcastConnections(nest, name, source);
});
function broadcastConnections(nest, name, exceptFor = null) {
for (let neighbor of nest.neighbors) {
if (neighbor == exceptFor) continue;
request(nest, neighbor, "connections", {
name,
neighbors: nest.state.connections.get(name)
});
}
}
everywhere(nest => {
nest.state.connections = new Map;
nest.state.connections.set(nest.name, nest.neighbors);
broadcastConnections(nest, nest.name);
});
Сравнение использует JSON.stringify, поскольку объекты или массивы с использованием == будут возвращать true только в том случае, если они полностью совпадают, что не то, что нам нужно здесь. Сравнение строк JSON — это простой и эффективный способ сравнения их содержимого.
Узлы немедленно начинают транслировать свои соединения, они должны немедленно предоставить каждому гнезду текущую карту сети, за исключением случаев, когда некоторые гнёзда полностью недоступны.
Вы можете делать с графиком то, что мы видели в главе 7. Если у нас есть маршрут к месту назначения сообщения, мы знаем, в каком направлении его отправлять.
Эта функция findRoute очень похожа на findRoute из главы 7, она ищет маршрут к сети для достижения заданного узла. Но вместо возврата всего маршрута он возвращает следующий шаг. Следующий узел будет использовать свою текущую информацию о сети, чтобы решить, куда отправить сообщение дальше.
function findRoute(from, to, connections) {
let work = [{at: from, via: null}];
for (let i = 0; i < work.length; i++) {
let {at, via} = work[i];
for (let next of connections.get(at) || []) {
if (next == to) return via;
if (!work.some(w => w.at == next)) {
work.push({at: next, via: via || next});
}
}
}
return null;
}
Теперь мы можем создать функцию для отправки длинных сообщений. Если сообщение отправляется прямому соседу, оно будет отправлено как обычно. ```
Если нет, то его следует заключить в объект и использовать "route"
тип запроса, чтобы отправить его к более близкому соседу, что приведёт к повторению этого поведения.
function routeRequest(nest, target, type, content) {
if (nest.neighbors.includes(target)) {
return request(nest, target, type, content);
} else {
let via = findRoute(nest.name, target, nest.state.connections);
if (!via) throw new Error(`No route to ${target}`);
return request(nest, via, "route", {target, type, content});
}
}
requestType("route", (nest, {target, type, content}) => {
return routeRequest(nest, target, type, content);
});
Теперь мы можем отправлять сообщения в птичье гнездо на церковной башне, которое находится в четырёх прыжках от нас.
routeRequest(bigOak, "Church Tower", "note", "Incoming jackdaws!");
Мы построили несколько слоёв функциональности поверх исходной коммуникационной системы, чтобы сделать её удобной в использовании. Это хорошая модель реальной работы компьютерной сети (хотя и упрощённая).
Одной из заметных особенностей компьютерных сетей является их ненадёжность — абстракции, построенные на них, могут помочь, но не могут абстрагироваться от сетевых сбоев. Поэтому сетевое программирование обычно связано с прогнозированием и обработкой сбоев.
async
функцииЧтобы сохранить важную информацию, как известно, вороны копируют её в своих гнёздах. Таким образом, если орёл разрушит одно гнездо, информация не будет потеряна.
Для поиска информации, которой нет в собственном хранилище, компьютер гнезда может запросить другие случайные гнёзда в сети, пока не найдёт компьютер другого гнезда.
requestType("storage", (nest, name) => storage(nest, name));
function findInStorage(nest, name) {
return storage(nest, name).then(found => {
if (found != null) return found;
else return findInRemoteStorage(nest, name);
});
}
function network(nest) {
return Array.from(nest.state.connections.keys());
}
function findInRemoteStorage(nest, name) {
let sources = network(nest).filter(n => n != nest.name);
function next() {
if (sources.length == 0) {
return Promise.reject(new Error("Not found"));
} else {
let source = sources[Math.floor(Math.random() * sources.length)];
sources = sources.filter(n => n != source);
return routeRequest(nest, source, "storage", name)
.then(value => value != null ? value : next(), next);
}
}
return next();
}
Поскольку connections
— это Map
, Object.keys
не работает. У него есть метод key
, но он возвращает итератор, а не массив. Можно использовать функцию Array.from
, чтобы преобразовать итератор (или итерируемый объект) в массив.
Даже при использовании Promise
это довольно неуклюжий код. Несколько асинхронных операций связаны между собой неявным образом. Нам снова нужна рекурсивная функция (next
) для моделирования обхода на гнезде.
Фактически код делает линейные вещи — он всегда ждёт завершения предыдущего действия перед началом следующего. В синхронной модели программирования выражение было бы проще.
Хорошая новость заключается в том, что JavaScript позволяет писать псевдосинхронный код. Асинхронная функция — это функция, которая неявно возвращает Promise
, и она может ждать других Promise
внутри своего тела, выглядя синхронно.
Можно переписать findInStorage
следующим образом:
async function findInStorage(nest, name) {
let local = await storage(nest, name);
if (local != null) return local;
let sources = network(nest).filter(n => n != nest.name);
while (sources.length > 0) {
let source = sources[Math.floor(Math.random() *
sources.length)];
sources = sources.filter(n => n != source);
try {
let found = await routeRequest(nest, source, "storage",
name);
if (found != null) return found;
} catch (_) {}
}
throw new Error("Not found");
}
Асинхронные функции помечаются ключевым словом async
, предшествующим объявлению функции. Методы также можно сделать асинхронными, добавив async
перед именем. Когда вызывается такая функция или метод, возвращается Promise
. Если тело возвращает что-то, Promise
разрешается. Если оно выбрасывает исключение, Promise
отклоняется.
findInStorage(bigOak, "events on 2017-12-21")
.then(console.log);
Внутри асинхронной функции await
можно поставить перед выражением, чтобы дождаться разрешения Promise
, а затем продолжить выполнение функции.
Такие функции не выполняются от начала до конца, как обычные функции JavaScript. Вместо этого они могут приостанавливаться в любом месте, где есть await
, и возобновляться позже.
Для осмысленного асинхронного кода этот маркер обычно более удобен, чем прямое использование Promise
. Даже если вам нужно делать вещи, которые не подходят для синхронной модели, такие как одновременное выполнение нескольких действий, легко комбинировать await
и прямое использование Promise
.
Способность функций приостанавливать, а затем возобновлять работу не уникальна для асинхронных функций. JavaScript также имеет функцию, называемую генератором. Они похожи, но без Promise
.
Когда вы определяете функцию с помощью function*
(добавляя звёздочку после имени функции), она становится генератором. При вызове генератора он возвращает итератор, который мы видели в главе 6.
function* powers(n) {
for (let current = n;; current *= n) {
yield current;
}
}
for (let power of powers(3)) {
if (power > 50) break;
console.log(power);
}
// → 3
// → 9
// → 27
Изначально, когда вы вызываете powers
, функция замораживается в начале. Каждый раз, когда вызывается итератор next
, функция запускается до тех пор, пока она не достигнет выражения yield
, которое приостанавливает её и делает значение, созданное выражением, следующим значением, возвращаемым итератором. Когда функция возвращается (что никогда не происходит в этом примере), итератор завершается.
Использование генераторов обычно намного проще, чем написание итераторов. Можно написать итератор типа group
(из упражнения в главе 6) с этим генератором:
Group.prototype[Symbol.iterator] = function*() {
for (let i = 0; i < this.members.length; i++) {
yield this.members[i];
}
};
Больше не нужно создавать объект для хранения состояния итерации — генератор автоматически сохраняет своё локальное состояние каждый раз, когда он yield
.
Выражение yield
может появляться только непосредственно в самой функции генератора, а не в определяемой вами внутренней функции. Состояние, сохранённое генератором при возврате (yield
), представляет собой просто его локальную среду и позицию, в которой он yield
».
Асинхронная функция является разновидностью генератора. Оно в момент вызова порождает Promise, который при завершении (выполнении) разрешается, а при возникновении исключения — отклоняется.
Каждый раз, когда оно выдаёт (await) Promise, результатом выражения await становится результат этого Promise: значение или исключение, которое было выброшено.
Асинхронные программы выполняются по частям. Каждая часть может запускать некоторые операции и планировать выполнение кода после завершения или сбоя этих операций. В промежутках между этими частями программа простаивает, ожидая следующего действия.
Поэтому функции обратного вызова не вызываются непосредственно кодом, который их планирует. Если из функции вызвать setTimeout, то к моменту вызова функции обратного вызова сама функция уже завершит работу. Когда функция обратного вызова завершается, управление не возвращается вызвавшей её функции.
Асинхронное поведение происходит в собственном стеке вызовов пустых функций. Это одна из причин, почему без Promise сложно управлять исключениями в асинхронном коде. Поскольку каждая функция обратного вызова начинает работу с почти пустым стеком, если они выбрасывают исключение, ваша catch-обработчик не будет находиться в стеке.
try {
setTimeout(() => {
throw new Error("Woosh");
}, 20);
} catch (_) {
// Этот код не выполнится
console.log("Caught!");
}
Независимо от того, насколько плотно происходят события (например, таймауты или входящие запросы), JavaScript-среда может выполнять только одну программу за раз. Можно представить, что вокруг программы вращается большой цикл, называемый циклом событий. Когда нечего делать, этот цикл останавливается. Но по мере поступления событий они добавляются в очередь, и их код выполняется по одному. Из-за отсутствия параллельного выполнения медленный код может задерживать обработку других событий.
В этом примере установлен таймаут, но затем занято время до момента истечения таймера, что приводит к задержке таймаута.
let start = Date.now();
setTimeout(() => {
console.log("Timeout ran at", Date.now() - start);
}, 20);
while (Date.now() < start + 50) {}
console.log("Wasted time until", Date.now() - start);
// → Wasted time until 50
// → Timeout ran at 55
Promise всегда разрешается или отклоняется как новое событие. Даже если Promise был разрешён, ожидание его приведёт к выполнению вашего обратного вызова после завершения текущей программы, а не сразу.
Promise.resolve("Done").then(console.log);
console.log("Me first!");
// → Me first!
// → Done
В следующих главах мы увидим, как различные типы событий обрабатываются в цикле событий.
Когда ваша программа работает синхронно, кроме того, что делает сама программа, ничего не меняется. Для асинхронных программ это не так — они могут иметь промежутки времени, в течение которых может выполняться другой код.
Рассмотрим пример. Одним из увлечений ворон является подсчёт количества цыплят, вылупляющихся каждый год в деревне. Эти данные хранятся в их памяти. Следующий код пытается перебрать все гнёзда за указанный год.
function anyStorage(nest, source, name) {
if (source == nest.name) return storage(nest, name);
else return routeRequest(nest, source, "storage", name);
}
async function chicks(nest, year) {
let list = "";
await Promise.all(network(nest).map(async name => {
list += `${name}: ${
await anyStorage(nest, name, `chicks in ${year}`)
}\n`;
}));
return list;
}
Часть async name => показывает, что стрелочные функции также можно сделать асинхронными, добавив перед ними ключевое слово async.
Код на первый взгляд выглядит нормально... он сопоставляет асинхронные стрелочные функции с набором гнёзд, создавая набор Promise, а затем использует Promise.all для ожидания всех Promise перед возвратом списка.
Но у него есть серьёзная проблема. Он всегда возвращает только одну строку вывода, перечисляя ответ самого медленного гнезда.
chicks(bigOak, 2017).then(console.log);
Можете ли вы объяснить, почему?
Проблема заключается в операторе +=, который принимает текущее значение list при запуске оператора и устанавливает list равным этому значению плюс новая строка после завершения await.
Однако существует асинхронный промежуток между временем начала выполнения оператора и временем его завершения. Оператор map выполняет итерацию по всем элементам перед добавлением чего-либо в список, поэтому каждый оператор += начинается с пустой строки и завершается установкой list в однострочный список — добавление результата этой строки.
Эту проблему легко решить, возвращая строку из Promise и вызывая join для результата Promise.all вместо изменения привязки для построения списка. Как обычно, вычисление нового значения менее подвержено ошибкам, чем изменение существующего.
async function chicks(nest, year) {
let lines = network(nest).map(async name => {
return name + ": " +
await anyStorage(nest, name, `chicks in ${year}`);
});
return (await Promise.all(lines)).join("\n");
}
Ошибки такого рода легко допустить, особенно при использовании await, и вы должны знать, где в коде возникают промежутки. Явное асинхронное выполнение JavaScript (будь то через обратные вызовы, Promise или await) облегчает обнаружение этих промежутков.
Асинхронное программирование позволяет ожидать длительных действий, не замораживая программу во время этих действий. Обычно в JavaScript используются функции обратного вызова для реализации этого стиля программирования, которые вызываются после завершения действий. Цикл событий управляет этими обратными вызовами, вызывая их по очереди в подходящее время, чтобы избежать перекрытия их выполнения.
Promise и асинхронные функции упрощают асинхронное программирование. Promise — это объект, представляющий действие, которое может быть выполнено в будущем. Кроме того, асинхронные функции позволяют писать асинхронные программы так же, как синхронные.
У воронов в деревне есть старый хирургический нож, которым они иногда пользуются для особых задач, таких как разрезание паутины или упаковка. Чтобы быстро отследить местонахождение ножа, каждый раз, когда нож перемещается в другое гнездо, добавляется запись в память владельца и того, кто взял нож, с именем «скальпель» и новым местоположением.
Это означает, что найти нож — значит проследить записи в памяти до тех пор, пока вы не найдёте гнездо, указывающее на себя.
Напишите асинхронную функцию locateScalpel, которая начинается с текущего гнезда. Вы можете использовать ранее определённую функцию anyStorage для доступа к памяти любого гнезда. Нож перемещался в течение длительного времени, и вы можете предположить, что в памяти каждого гнезда есть запись «скальпель».
Затем напишите ещё одну версию той же функции, но без использования async и await.
Правильно ли отображаются сбои в обеих версиях? Как это реализовано?
async function locateScalpel(nest) {
// Ваш код здесь.
}
function locateScalpel2(nest) {
// Ваш код здесь.
}
locateScalpel(bigOak).then(console.log);
// → Butcher Shop
Учитывая массив Promise, Promise.all возвращает Promise, ожидающий завершения всех Promise в массиве. Затем он успешно создаёт массив результатов. Если один из Promise в массиве терпит неудачу, этот Promise также терпит неудачу, и причина неудачи исходит от этого неудачного Promise.
Реализуйте свою собственную обычную функцию с именем Promise_all.
Помните, что после успешного или неудачного выполнения Promise он не может снова стать успешным или неудачным, и дальнейшие вызовы функции разрешения будут игнорироваться. Это может упростить обработку сбоев Promise.
function Promise_all(promises) {
return new Promise((resolve, reject) => {
// Ваш код здесь.
});
}
// Тестовый код.
Promise_all([]).then(array => {
console.log("This should be []:", array);
});
function soon(val) {
return new Promise(resolve => {
setTimeout(() => resolve(val), Math.random() * 500);
});
}
Promise_all([soon(1), soon(2), soon(3)]).then(array => {
console.log("This
**Перевод выполнен с учётом контекста и стилистики исходного текста.** ```
should be [1, 2, 3]:", array);
});
Promise_all([soon(1), Promise.reject("X"), soon(3)])
.then(array => {
console.log("We should not get here");
})
.catch(error => {
if (error != "X") {
console.log("Unexpected failure:", error);
}
});
Этот фрагмент написан на языке JavaScript.
В тексте присутствуют конструкции языка, которые не имеют прямого перевода на русский язык. В данном случае это названия функций и методов: soon
, Promise.reject
.
Перевод выполнен с сохранением оригинального форматирования.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )