Разве обещания не являются просто обратными вызовами?

430

Я разрабатывал JavaScript в течение нескольких лет, и я совсем не понимаю суеты по поводу обещаний.

Кажется, что все, что я делаю, это изменения:

api(function(result){
    api2(function(result2){
        api3(function(result3){
             // do work
        });
    });
});

Для чего я мог бы использовать библиотеку async для чего-то вроде:

api().then(function(result){
     api2().then(function(result2){
          api3().then(function(result3){
               // do work
          });
     });
});

Который более кодовый и менее читаемый. Я ничего не получил здесь, это не внезапно волшебно "плоский" также. Не говоря уже о необходимости превращать вещи в обещания.

Так в чем же суть обещаний?

Бенджамин Грюнбаум
источник
11
По теме : на Html5Rocks есть действительно информативная статья об обещаниях: html5rocks.com/en/tutorials/es6/promises
ComFreek,
2
К вашему сведению, ответ, который вы приняли, - это тот же старый список тривиальных преимуществ, которые вообще не являются предметом обещаний и даже не убедили меня использовать обещания: /. Что убедило меня использовать обещания, так это аспект DSL, описанный в ответе Оскара
Esailija
@Esailija хорошо, твои слова убедили меня. Я принял другой ответ, хотя я думаю, что ответ Берги поднимает действительно хорошие (и разные) моменты.
Бенджамин Грюнбаум
@Esailija "Что убедило меня использовать обещания, так это аспект DSL, описанный в ответе Оскара" << Что такое "DSL"? и что такое «аспект DSL», на который вы ссылаетесь?
Monsto
1
@monsto: DSL: язык, специфичный для предметной области, язык, специально разработанный для использования в определенном подмножестве системы (например, SQL или ORM для общения с базой данных, регулярное выражение для поиска шаблонов и т. д.). В этом контексте «DSL» - это API-интерфейс Promise, который, если вы структурируете свой код так, как это делал Оскар, почти как синтаксический сахар, который дополняет JavaScript для решения конкретного контекста асинхронных операций. Обещания создают некоторые идиомы, которые превращают их в почти язык, разработанный, чтобы позволить программисту легче понять несколько неуловимый умственный поток этого типа структур.
Майкл Экока

Ответы:

632

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

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
});

Конечно, не намного меньше кода, но гораздо более читабельно.

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

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
});

Почти так же, как try { ... } catchблок.

Даже лучше:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
}).then(function() {
     //do something whether there was an error or not
     //like hiding an spinner if you were performing an AJAX request.
});

А еще лучше: Что делать , если эти 3 вызовов api, api2, api3могут работать одновременно (например , если они были AJAX звонков) , но вам нужно ждать три? Без обещаний вам придется создать какой-то счетчик. С обещаниями, используя обозначение ES6, это еще один кусок пирога и довольно аккуратный:

Promise.all([api(), api2(), api3()]).then(function(result) {
    //do work. result is an array contains the values of the three fulfilled promises.
}).catch(function(error) {
    //handle the error. At least one of the promises rejected.
});

Надеюсь, вы видите Обещания в новом свете.

Оскар Пас
источник
124
Им действительно не следовало называть это «Обещанием». «Будущее» как минимум в 100 раз лучше.
Pacerier
12
@Pacerier, потому что будущее не испорчено jQuery?
Esailija
5
Альтернативный шаблон (в зависимости от того, что нужно: api (). Then (api2) .then (api3) .then (doWork); то есть, если функции api2 / api3 получают входные данные с последнего шага и сами возвращают новые обещания, они можно просто приковать цепью без лишней обертки. То есть они сочиняют.
Dtipson
1
Что делать, если есть асинхронные операции в api2и api3? будет ли последний .thenвызываться только после завершения этих асинхронных операций?
NiCk Newman
8
Почему вы меня пометили? Я просто немного исправил грамматику. Я не эксперт JS. :)
Скотт Аркишевский
169

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

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

Так в чем же основная идея?

Обещания - это объекты, представляющие результат одного (асинхронного) вычисления. Они разрешают этот результат только один раз. Есть несколько вещей, что это значит:

Обещания реализуют шаблон наблюдателя:

  • Вам не нужно знать обратные вызовы, которые будут использовать значение до завершения задачи.
  • Вместо ожидания обратных вызовов в качестве аргументов ваших функций, вы можете легко return получить объект Promise.
  • Обещание будет хранить значение, и вы можете прозрачно добавить обратный вызов, когда захотите. Он будет вызван, когда будет доступен результат. «Прозрачность» подразумевает, что когда у вас есть обещание и вы добавляете к нему обратный вызов, для вашего кода не имеет значения, достигнут ли результат - API и контракты одинаковы, что значительно упрощает кэширование / запоминание.
  • Вы можете легко добавить несколько обратных вызовов

Обещания являются связными ( монадические , если хотите ):

  • Если вам нужно преобразовать значение, которое представляет обещание, вы сопоставляете функцию преобразования с обещанием и получаете новое обещание, которое представляет преобразованный результат. Вы не можете синхронно получить значение, чтобы использовать его как-то, но вы можете легко поднять преобразование в контексте обещания. Нет стандартных обратных вызовов.
  • Если вы хотите связать две асинхронные задачи, вы можете использовать .then()метод. Он будет принимать обратный вызов, который будет вызван с первым результатом, и возвращает обещание для результата обещания, которое возвращает обратный вызов.

Звучит сложно? Время для примера кода.

var p1 = api1(); // returning a promise
var p3 = p1.then(function(api1Result) {
    var p2 = api2(); // returning a promise
    return p2; // The result of p2 …
}); // … becomes the result of p3

// So it does not make a difference whether you write
api1().then(function(api1Result) {
    return api2().then(console.log)
})
// or the flattened version
api1().then(function(api1Result) {
    return api2();
}).then(console.log)

Сглаживание не происходит волшебным образом, но вы можете легко это сделать. Для вашего сильно вложенного примера, (почти) эквивалент будет

api1().then(api2).then(api3).then(/* do-work-callback */);

Если просмотр кода этих методов помогает понять, вот основная библиотека обещаний в нескольких строках .

В чем суета обещаний?

Абстракция Promise позволяет намного лучше комбинировать функции. Например, рядом с thenцепочкой allфункция создает обещание для комбинированного результата нескольких обещаний параллельного ожидания.

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

Не говоря уже о необходимости превращать вещи в обещания.

Это довольно тривиально на самом деле с хорошими библиотеками обещаний, см. Как преобразовать существующий API обратного вызова в обещания?

Берги
источник
Привет, Берги, не могли бы вы добавить что-нибудь интересное к такому вопросу? stackoverflow.com/questions/22724883/…
Себастьян Лорбер
1
@Sebastien: я не знаю много о Scala (пока), и я мог только повторить то, что сказал Бенджамин :-)
Bergi
3
Небольшое замечание: вы не можете использовать .then(console.log), так как console.log зависит от контекста консоли. Таким образом, это приведет к ошибке недопустимого вызова. Используйте console.log.bind(console)или x => console.log(x)для привязки контекста.
Тамас Хегедус
3
@hege_hegedus: Существуют среды, в которых consoleметоды уже связаны. И, конечно, я только сказал, что оба вложения имеют абсолютно одинаковое поведение, а не то, что любое из них будет работать :-P
Bergi
1
Это было здорово. Это то, что мне было нужно: меньше кода и больше интерпретации. Спасибо.
Адам Паттерсон
21

В дополнение к уже установленным ответам, с помощью стрелок ES6 функции Promises превращаются из скромно сияющего маленького синего карлика прямо в красного гиганта. Это вот-вот рухнет в сверхновую

api().then(result => api2()).then(result2 => api3()).then(result3 => console.log(result3))

Как отметил олигофрен , без аргументов между вызовами API вам не нужны анонимные функции-оболочки:

api().then(api2).then(api3).then(r3 => console.log(r3))

И, наконец, если вы хотите достичь сверхмассивного уровня черной дыры, можно ожидать Promises:

async function callApis() {
    let api1Result = await api();
    let api2Result = await api2(api1Result);
    let api3Result = await api3(api2Result);

    return api3Result;
}
Джон Вайс
источник
9
"с функциями стрелок ES6 Обещания превращаются из скромно сияющей маленькой голубой звезды прямо в красного гиганта. Это скоро превратится в сверхновую" Перевод: Объединение функций стрелок ES6 с Обещаниями - это здорово :)
user3344977
3
Это делает Обещания звучать как космическая катастрофа, которую я не думаю, что было вашим намерением.
Майкл Макгиннис
Если вы не используете аргументы в apiXметодах, вы можете также пропустить функции со стрелками в целом: api().then(api2).then(api3).then(r3 => console.log(r3)).
oligofren
@MichaelMcGinnis - Благотворное влияние Promises на унылый адский колл напоминает взрывающуюся сверхновую в темном углу пространства.
Джон Вайс
Я знаю, что вы имеете в виду это поэтично, но обещания довольно далеки от "сверхновой". На ум приходит нарушение монадического закона или отсутствие поддержки для более мощных вариантов использования, таких как отмена или возврат нескольких значений.
Дмитрий Зайцев
15

В дополнение к удивительным ответам выше, можно добавить еще 2 пункта:

1. Семантическая разница:

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

И наоборот, обратные вызовы обрабатывают события. Таким образом, если интересующее вас событие произошло до того, как обратный вызов был зарегистрирован, обратный вызов не вызывается.

2. Инверсия контроля

Обратные вызовы включают инверсию управления. Когда вы регистрируете функцию обратного вызова в любом API, среда выполнения Javascript сохраняет функцию обратного вызова и вызывает ее из цикла событий, как только она будет готова к запуску.

См. Цикл событий Javascript для объяснения.

В Promises управление находится в вызывающей программе. Метод .then () может быть вызван в любое время, если мы сохраним объект обещания.

DWW
источник
1
Я не знаю почему, но это кажется лучшим ответом.
radiantshaw
13

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

// Sequentially:
api1()
  .then(r1 => api2(r1))
  .then(r2 => api3(r2))
  .then(r3 => {
      // Done
  });

// Parallel:
Promise.all([
    api1(),
    api2(),
    api3()
]).then(([r1, r2, r3]) => {
    // Done
});
Дункан Лук
источник
5

Обещания не являются обратными вызовами, оба являются идиомами программирования, которые облегчают асинхронное программирование. Использование асинхронного / await-стиля программирования с использованием сопрограмм или генераторов, которые возвращают обещания, может рассматриваться как третья такая идиома. Сравнение этих идиом в разных языках программирования (включая Javascript) здесь: https://github.com/KjellSchubert/promise-future-task

Кьелл Шуберт
источник
5

Нет, совсем нет.

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

На самом деле, в JavaScript сами функции рассматриваются как объекты и, следовательно, как и все другие объекты, даже функции можно отправлять в качестве аргументов другим функциям . Наиболее распространенным и общим вариантом использования, который можно себе представить, является функция setTimeout () в JavaScript.

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

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

Аюш Джайн
источник
2

Никаких обещаний - это просто обертка для обратных вызовов

пример Вы можете использовать нативные обещания JavaScript с узлом js

my cloud 9 code link : https://ide.c9.io/adx2803/native-promises-in-node

/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
    request.get(url, function (error, response, body) {
    if (!error && response.statusCode == 200) {
        resolve(body);
    }
    else {
        reject(error);
    }
    })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
//get the post with post id 100
promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
var obj = JSON.parse(result);
return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
})
.catch(function (e) {
    console.log(e);
})
.then(function (result) {
    res.end(result);
}
)

})


var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("Example app listening at http://%s:%s", host, port)

})


//run webservice on browser : http://localhost:8081/listAlbums
Apoorv
источник
1

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

Хамид Шоджа
источник
0

Обзор обещаний:

В JS мы можем заключать в обещания асинхронные операции (например, вызовы базы данных, вызовы AJAX). Обычно мы хотим запустить некоторую дополнительную логику на полученных данных. Обещания JS имеют функции-обработчики, которые обрабатывают результат асинхронных операций. Функции-обработчики могут даже содержать другие асинхронные операции, которые могут полагаться на значение предыдущих асинхронных операций.

Обещание всегда имеет 3 следующих состояния:

  1. в ожидании: начальное состояние каждого обещания, ни выполненного, ни отклоненного.
  2. выполнено: операция успешно завершена.
  3. отклонено: операция не выполнена.

Ожидающее обещание может быть разрешено / выполнено или отклонено со значением. Затем вызываются следующие методы-обработчики, которые принимают обратные вызовы в качестве аргументов:

  1. Promise.prototype.then() : Когда обещание будет выполнено, будет вызван аргумент обратного вызова этой функции.
  2. Promise.prototype.catch() : Когда обещание отклонено, будет вызван аргумент обратного вызова этой функции.

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

пример

function createProm(resolveVal, rejectVal) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                console.log("Resolved");
                resolve(resolveVal);
            } else {
                console.log("Rejected");
                reject(rejectVal);
            }
        }, 1000);
    });
}

createProm(1, 2)
    .then((resVal) => {
        console.log(resVal);
        return resVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
        return resVal + 2;
    })
    .catch((rejectVal) => {
        console.log(rejectVal);
        return rejectVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
    })
    .finally(() => {
        console.log("Promise done");
    });

  • Функция createProm создает обещания, которые разрешаются или отклоняются на основе случайного Nr через 1 секунду
  • Если обещание выполнено первым then метод, и разрешенное значение передается в качестве аргумента обратного вызова.
  • Если обещание отклонено первым catch метод, а отклоненное значение передается в качестве аргумента.
  • catchИ thenметоды возвращают обещания, поэтому мы можем приковать их. Они переносят любое возвращаемое значение Promise.resolveи любое выброшенное значение (используя throwключевое слово) вPromise.reject . Таким образом, любое возвращаемое значение преобразуется в обещание, и в этом обещании мы снова можем вызвать функцию-обработчик.
  • Цепочки обещаний дают нам более точно настроенный контроль и лучший обзор, чем вложенные обратные вызовы. Например, catchметод обрабатывает все ошибки, которые произошли до catchобработчика.
Виллем ван дер Веен
источник