Не обіцянки - це лише зворотні дзвінки?


430

Я розробляв JavaScript вже кілька років, і взагалі не розумію суєти щодо обіцянок.

Здається, що все, що я роблю, - це зміни:

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

Яка я могла б використовувати бібліотеку, як асинхронність, у будь-якому випадку:

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

Що більше коду і менш читабельне. Я тут нічого не здобув, і це не раптом магічно "плоско". Не кажучи вже про необхідність перетворювати речі на обіцянки.

Отже, яка велика метушня щодо обіцянок тут?


11
On-topic : є справді інформативна стаття про Обіцянки на Html5Rocks: html5rocks.com/en/tutorials/es6/promises
ComFreek

2
Фій, відповідь, яку ви прийняли, - це той самий старий перелік дрібницьких переваг, які зовсім не суть обіцянок і навіть не переконували мене використовувати обіцянки: /. Що мене переконало у використанні обіцянок - це аспект DSL, як описано у відповіді Оскара
Есаїлія

@Esailija чудово, твій нехай говорять мене переконали. Я прийняв іншу відповідь, хоча думаю, що й Бергі також викликає кілька справді хороших (і різних) балів.
Бенджамін Грюнбаум

@Esailija "Що мене переконало у використанні обіцянок - це аспект DSL, як описано у відповіді Оскара" << Що таке "DSL"? і що таке "аспект DSL", про який ви посилаєтесь?
Монсто

1
@monsto: DSL: Мова, визначена для домену, мова, призначена спеціально для використання в певному підмножині системи (наприклад, SQL або ORM для розмови з базою даних, регулярного вираження для пошуку шаблонів тощо). У цьому контексті «DSL» - це API Promise, який, якщо ви структуруєте свій код так, як це робив Оскар, майже як синтаксичний цукор, який доповнює JavaScript для вирішення конкретного контексту операцій з асинхронізацією. Обіцяння створюють деякі ідіоми, які перетворюють їх майже на мову, розроблену для того, щоб програміст легше зрозумів дещо невловимий ментальний потік цього типу структур.
Майкл Екока

Відповіді:


631

Обіцянки - це не зворотні дзвінки. Обіцянка представляє майбутній результат асинхронної операції . Звичайно, записуючи їх так, як ви робите, ви отримуєте мало користі. Але якщо ви пишете їх так, як вони призначені для використання, ви можете писати асинхронний код таким чином, що нагадує синхронний код і набагато простіше в ньому слідувати:

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 майбутнє не пошкодив?
Есаїлія

5
Альтернативний малюнок (залежно від того, що потрібно: api (). Тоді (api2). Потім (api3). Далі (doWork); тобто якщо функції api2 / api3 беруть вклад з останнього кроку і самі повертають нові обіцянки, вони можна просто прикувати ланцюг без додаткового обгортання. Тобто вони складають.
Dtipson

1
Що робити, якщо в асинхронних операціях є api2і api3? чи .thenбуде викликано останнє лише після завершення цих операцій з асинхронізацією?
NiCk Newman

8
Чому ви позначили мене? Я трохи поправив граматику. Я не експерт JS. :)
Скотт Арчішевський

169

Так, обіцянки - це асинхронні зворотні дзвінки. Вони не можуть зробити нічого, чого зворотний виклик не може зробити, і ви стикаєтесь з тими ж проблемами з асинхронністю, що і з простими зворотними викликами.

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

То яка головна ідея?

Обіцянки - це об'єкти, що представляють результат одного (асинхронного) обчислення. Вони вирішують цей результат лише один раз. Є кілька речей, що це означає:

Обіцяє застосовувати схему спостерігачів:

  • Вам не потрібно знати зворотні дзвінки, які використовуватимуть значення до завершення завдання.
  • Замість того, щоб очікувати зворотних дзвінків як аргументів вашим функціям, ви можете легко returnоб'єкт Обіцяти
  • Обіцянка збереже значення, і ви можете прозоро додавати зворотний дзвінок, коли захочете. Він буде викликаний, коли результат буде доступний. "Прозорість" означає, що коли у вас є обіцянка та додавання зворотного дзвінка до неї, це не має значення для вашого коду, чи результат вже прийшов - 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 */);

Якщо бачення коду цих методів допомагає зрозуміти, ось найосновніший пробіл із обіцянками в декількох рядках .

Яка велика метушня щодо обіцянок?

Абстракція «Обіцяння» дозволяє значно краще поєднувати функції. Наприклад, поруч із thenланцюжком allфункція створює обіцянку для комбінованого результату декількох обіцянок паралельно очікування.

Останнє, але не менш важливе, обіцянки мають інтегровану обробку помилок. Результатом обчислень може бути те, що або обіцянка виконується зі значенням, або вона відкидається з причиною. Усі функції композиції справляються з цим автоматично і розповсюджують помилки в ланцюгах обіцянок, так що вам не потрібно про це піклуватися явно скрізь - на відміну від простої реалізації зворотного виклику. Зрештою, ви можете додати спеціальний зворотний виклик помилок для всіх винятків, що відбулися.

Не кажучи вже про необхідність перетворювати речі на обіцянки.

Це досить тривіально, з хорошими бібліотеками з обіцянками, див. Як перетворити існуючий API зворотного виклику в обіцянки?


привіт Бергі, чи хотіли б ти щось цікаве додати до цього питання? stackoverflow.com/questions/22724883/…
Себастьян Лорбер

1
@Sebastien: Я не знаю багато про Скалу (поки що), і я міг лише повторити те, що сказав Бенджамін :-)
Бергі,

3
Лише невелике зауваження: ви не можете використовувати .then(console.log), оскільки console.log залежить від контексту консолі. Таким чином це призведе до незаконної помилки виклику. Використовувати console.log.bind(console)або x => console.log(x)прив'язувати контекст.
Тамас Гегед

3
@hege_hegedus: Є середовища, де consoleметоди вже пов'язані. І звичайно, я лише сказав, що обидва гніздування мають однакову поведінку, не те, що будь-яке з них спрацює :-P
Бергі

1
Це було чудово. Це те, що мені було потрібно: менше коду та більше інтерпретації. Дякую.
Адам Паттерсон

21

Окрім уже встановлених відповідей, за допомогою функцій стрілок ES6 Обіцянки перетворюються із скромно сяючого маленького синього карлика прямо у червоного гіганта. Це ось-ось перевалиться в наднову:

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

Як зазначав олігофрен , без аргументів між api-дзвінками вам взагалі не потрібні функції анонімної обгортки:

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

І нарешті, якщо ви хочете досягти надмасивного рівня чорної діри, обіцянки можна чекати:

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

    return api3Result;
}

9
"з функціями стрілок ES6 Обіцянки перетворюються із скромно сяючої маленької синьої зірки прямо в червоного гіганта. Це ось-ось обрушиться на наднову" Переклад: Поєднання функцій стрілок ES6 із Обіцянками є приголомшливим :)
користувач3344977

3
Через це Обіцяння звучать як космічна катастрофа, яка, на мою думку, не була вашою метою.
Майкл МакГінніс

Якщо ви не використовуєте аргументи в apiXметодах, ви можете також пропустити функції зі стрілками в цілому: api().then(api2).then(api3).then(r3 => console.log(r3)).
олігофрен

@MichaelMcGinnis - сприятливий вплив Обіцянь на туге пекло зворотного виклику - це як вибух наднової в темному куточку простору.
Джон Вайс

Я знаю, ви це маєте на увазі поетично, але обіцянки досить далекі від "наднової". Приходить до уваги порушення монадійного закону або відсутність підтримки для більш потужних випадків використання, таких як скасування або повернення кількох цінностей.
Дмитро Зайцев

15

Окрім дивовижних відповідей вище, можна додати ще 2 бали:

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

Обіцянки можуть бути вже вирішені після створення. Це означає, що вони гарантують умови, а не події . Якщо вони вже вирішені, розв'язана функція, передана їй, все ще називається.

І навпаки, зворотні виклики обробляють події. Отже, якщо подія, яка вас цікавить, сталася до того, як зворотний виклик був зареєстрований, зворотний виклик не викликається.

2. Інверсія управління

Відкликання викликів передбачає інверсію управління. Коли ви реєструєте функцію зворотного дзвінка в будь-якому API, програма Javascript зберігає функцію зворотного дзвінка і викликає її з циклу подій, як тільки вона буде готова до запуску.

Зверніться Для пояснення циклу подій Javascript .

За допомогою Обіцянь контроль знаходиться у програмі виклику. Метод .then () може бути викликаний у будь-який час, якщо ми зберігаємо об'єкт обіцянки.


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

Обіцянки не є зворотними дзвінками, обидва - це ідіоми програмування, що полегшують програмування асинхронізації. Використання асинхронного / очікуваного стилю програмування з використанням процедур або генераторів, які повертають обіцянки, можна вважати третьою такою ідіомою. Порівняння цих ідіом у різних мовах програмування (включаючи 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

1

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


0

Обіцяє огляд:

У JS ми можемо зафіксувати асинхронні операції (наприклад, дзвінки до бази даних, дзвінки AJAX) у обіцянках. Зазвичай ми хочемо виконати деяку додаткову логіку на отриманих даних. У обіцянках JS є функції обробника, які обробляють результат асинхронних операцій. Функції обробника можуть мати навіть інші асинхронні операції всередині них, які можуть покладатися на значення попередніх асинхронних операцій.

Обіцянка завжди має три наступні стани:

  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обробником.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.