Оригинал: stephanboyer.com
Что такое promise?
Вообще говоря, promises (дословно — “обещания”) — это обёртки для функций обратного вызова (callback). Их можно использовать для упорядочивания синхронных и асинхронных действий.
С помощью promises вы сможете переписать:
$.ajax({
url: '/value',
success: function(value) {
var result = parseInt(value) + 10;
setTimeout(function() {
console.log(result);
}, 5000);
}
});
… в таком виде:
ajax({ url: '/value' }).then(function(value) {
return parseInt(value) + 10;
}).then(
delay(5000)
).then(function(result) {
console.log(result);
})
Может быть непонятно, почему нижний вариант лучше — а лучше он потому, что в нём отсутствует вложенность. Представьте, что наш пример гораздо длиннее. Верхний вариант имел бы дюжину вложенных функций и был бы очень трудночитаем, тогда как нижняя версия представляла бы собой прямолинейную последовательность действий и мало отличалась бы от случая синхронных действий.
Обещание поддерживает 2 метода: then
и catch
. then
принимает продолжение, являющееся функцией обратного вызова, принимающей результат в качестве аргумента и возвращающей новое обещание или другое значение. Аналогично, catch
— это callback, вызываемый при возникновении исключения или другой ошибки.
Полная версия then
в себя оба поведения:
promise.then(onFullfilled, onRejected)
Так, catch
можно определить следующим образом:
promise.catch(onRejected) := promise.then(null, onRejected)
Как создать обещание?
Для создания обещания вам не нужно создавать объект с методами then
и catch
. Вместо этого можно использовать конструктор Promise
:
var promise = new Promise(function(resolve, reject) {
// maybe do some async stuff in here
resolve('result');
});
Достаточно вызвать resolve
, когда обещание выполнено, или вызвать reject
, если что-то пошло не так. Также можно сгенерировать исключение. Если вы хотите обернуть значение в обещание, которое будет выполнено немедленно, можно просто написать Promise.resolve(value)
. В обратном случае достаточно написать Promise.reject(error)
.
Таймауты
Функция setTimeout
используется для выполнения кода после определенной задержки. Вот её версия, реализованная с помощью promises:
function delay(milliseconds) {
return function(result) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(result);
}, milliseconds);
});
};
}
Вот пример использования:
delay(1000)('hello').then(function(result) {
console.log(result);
});
Вы можете удивиться, что delay
каррирована. Это сделано для того, чтобы её можно было включить в последовательность после другого обещания:
somePromise.then(delay(1000)).then(function() {
console.log('1 second after somePromise!');
});
AJAX
AJAX — классический пример использования promises. Если вы используете jQuery, вы увидите, что $.ajax
не вполне совместима с promises, потому что не поддерживает catch
. Но мы с легкостью можем создать обёртку:
function ajax(options) {
return new Promise(function(resolve, reject) {
$.ajax(options).done(resolve).fail(reject);
});
}
Пример:
ajax({ url: '/' }).then(function(result) {
console.log(result);
});
Или, если вы не пользуетесь jQuery, вы можете создать обёртку для XMLHttpRequest
:
function ajax(url, method, data) {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.responseType = 'text';
request.onreadystatechange = function() {
if (request.readyState === XMLHttpRequest.DONE) {
if (request.status === 200) {
resolve(request.responseText);
} else {
reject(Error(request.statusText));
}
}
};
request.onerror = function() {
reject(Error("Network Error"));
};
request.open(method, url, true);
request.send(data);
});
}
Пример:
ajax('/', 'GET').then(function(result) {
console.log(result);
});
Отложенное исполнение
Вопрос: в каком порядке будут выведены строки после исполнения этого кода?
new Promise(function(resolve, reject) {
console.log('A');
resolve();
}).then(function() {
console.log('B');
});
console.log('C');
Ответ: A
, C
, затем B
.
Удивительно, правда? then
–переходник отложен, а тот, что передан в конструктор Promise
— нет. Но мы можем воспользоваться таким поведением then
. Часто хочется отложить выполнение некоторого куска кода до завершения асинхронной части. Раньше для этого можно было использовать setTimeout(func, 1)
но теперь для этого можно написать обещание:
var defer = Promise.resolve();
Его можно использовать так:
defer.then(function() {
console.log('A');
});
console.log('B');
Этот код выведет B
, затем A
. Хотя это и не короче, чем setTimeout(func, 1)
, это разъясняет наши намерения и совместимо с другими обещаниями. Таким образом, мы получаем более структурированный код.
Финальные замечания
Истинная ценность обещаний проявляется, когда весь асинхронный код структурирован с их помощью. Используя компонуемые обещания для таймеров или AJAX-действий, легко избежать вложенности и писать более линейный код.
Я был удивлён несколькими вещами:
- Функции обратного вызова для
then
иcatch
могут возвращать любое значение, но они ведут себя по-другому. Обычно, возвращаемое значение передаётся следующему заthen
выражению в цепочке. Но если это значение — обещание, дальше передаётся значение, возвращаемое обещанием при его выполнении. Это значит, что возвратPromise.resolve(x)
эквивалентен возвратуx
. - Функция, переданная в конструктор
Promise
, выполняется синхронно, но любые продолжения черезthen
илиcatch
будут отложены до следующего цикла событий. Этим объясняется работаdefer
. catch
— это зарезервированное ключевое слово, используемое для обработки исключений в JavaScript, но это также и имя метода, закрепляющего обработчик ошибок в обещании. Это похоже на неудачную коллизию имён. С другой стороны, это облегчает запоминание, так что не всё так плохо!
Первый пункт кажется мне странным из-за следующего мысленного эксперимента: что, если мы действительно хотим передать обещание следующей функции в цепочке? Если мы попытаемся сделать это таким наивным образом, обещание лишится обёртки, и мы не получим желаемый результат. Вы можете спросить: зачем вообще передавать обещание в callback? Ну, может быть, код не знает, какого типа значение он передаёт в функцию. Может быть, значение создаётся где-то ещё в программе, и оно просто должно быть передано. Теперь нужно сперва проверить, если это обещание, и в этом случае обернуть его во что-то ещё для защиты на время передачи. Поэтому я бы предпочёл, чтоб функции обратного вызова всегда должны были возвращать обещание (даже если значение обёрнуто в Promise.resolve(value)
).
Обдумав второй пункт, я пришёл к выводу, что функции обратного вызова должны быть отложенными. И вот почему: допустим, обещание не выполнено. Оно должно быть передано следующему в цепочке обработчику ошибок, или же сгенерировать исключение при отсутствии оного. Но ему сперва придётся дождаться, пока обработчики ошибок будут прикреплены. И как долго ждать? Очевидно: до следующей итерации цикла событий.
Несмотря на эти странности, promises — приятное дополнение к JavaScript. Ад callback’ов был одним из моих самых нелюбимых аспектов JavaScript, но теперь его можно избежать.
P.S. Promise
— это монада! Ну или, как минимум, могло бы ей быть, если бы не первый пункт.
Не смешно? А здесь смешно: @ithumor