Як реалізується бібліотека обіцянки / відкладання? [зачинено]


74

Як реалізується бібліотека обіцянки / відкладання типу q ? Я намагався прочитати вихідний код, але зрозуміти це було досить важко, тому я подумав, що було б чудово, якби хтось міг пояснити мені з високого рівня, які методи використовуються для реалізації обіцянок в однопоточних середовищах JS як Node та браузери.


Відповіді:


149

Мені важче пояснити, ніж показати приклад, тому ось дуже проста реалізація того, що може бути відстрочкою / обіцянкою.

Застереження: Це не функціональна реалізація, і деякі частини специфікації Promise / A відсутні, це лише для пояснення основи обіцянок.

tl; dr: Перейдіть до розділу Створення класів та прикладу, щоб побачити повну реалізацію.

Обіцянка:

Спочатку нам потрібно створити об’єкт-обіцянку з масивом зворотних викликів. Я почну працювати з об'єктами, тому що це зрозуміліше:

var promise = {
  callbacks: []
}

тепер додайте зворотні виклики методом тоді:

var promise = {
  callbacks: [],
  then: function (callback) {
    callbacks.push(callback);
  }
}

І нам також потрібні зворотні виклики помилок:

var promise = {
  okCallbacks: [],
  koCallbacks: [],
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
}

Відкласти:

Тепер створіть об'єкт defer, який буде мати обіцянку:

var defer = {
  promise: promise
};

Відстрочку потрібно вирішити:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },
};

І потрібно відхилити:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

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

І це те, що потрібно для базового відкладання / обіцянки впровадження.

Створіть класи та приклад:

Тепер давайте перетворимо обидва об’єкти на класи, спочатку обіцянку:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
};

А тепер відстрочка:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

І ось приклад використання:

function test() {
  var defer = new Defer();
  // an example of an async call
  serverCall(function (request) {
    if (request.status === 200) {
      defer.resolve(request.responseText);
    } else {
      defer.reject(new Error("Status code was " + request.status));
    }
  });
  return defer.promise;
}

test().then(function (text) {
  alert(text);
}, function (error) {
  alert(error.message);
});

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

Defer.all(promiseA, promiseB, promiseC).then()

або ланцюжок обіцянок:

getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);

Щоб прочитати більше про технічні характеристики: CommonJS Promise Specification . Зверніть увагу, що основні бібліотеки (Q, when.js, rsvp.js, node-promis, ...) відповідають специфікаціям Promises / A.

Сподіваюся, я був досить чітким.

Редагувати:

Як запитували в коментарях, я додав дві речі в цій версії:

  • Тоді можливість дати обіцянку, незалежно від того, який статус вона має.
  • Можливість ланцюга обіцянок.

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

Щоб мати змогу ланцюжково обіцяти, вам потрібно створити нову відстрочку для кожного дзвінка thenта, коли обіцянка вирішена / відхилена, вирішити / відхилити нову обіцянку з результатом зворотного дзвінка. Отже, коли обіцянка виконана, якщо зворотний виклик повертає нову обіцянку, вона прив’язується до обіцянки, поверненої разом із then(). Якщо ні, обіцянка вирішується з результатом зворотного дзвінка.

Ось обіцянка:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  status: 'pending',
  error: null,

  then: function (okCallback, koCallback) {
    var defer = new Defer();

    // Add callbacks to the arrays with the defer binded to these callbacks
    this.okCallbacks.push({
      func: okCallback,
      defer: defer
    });

    if (koCallback) {
      this.koCallbacks.push({
        func: koCallback,
        defer: defer
      });
    }

    // Check if the promise is not pending. If not call the callback
    if (this.status === 'resolved') {
      this.executeCallback({
        func: okCallback,
        defer: defer
      }, this.data)
    } else if(this.status === 'rejected') {
      this.executeCallback({
        func: koCallback,
        defer: defer
      }, this.error)
    }

    return defer.promise;
  },

  executeCallback: function (callbackData, result) {
    window.setTimeout(function () {
      var res = callbackData.func(result);
      if (res instanceof Promise) {
        callbackData.defer.bind(res);
      } else {
        callbackData.defer.resolve(res);
      }
    }, 0);
  }
};

І відстрочка:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    var promise = this.promise;
    promise.data = data;
    promise.status = 'resolved';
    promise.okCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, data);
    });
  },

  reject: function (error) {
    var promise = this.promise;
    promise.error = error;
    promise.status = 'rejected';
    promise.koCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, error);
    });
  },

  // Make this promise behave like another promise:
  // When the other promise is resolved/rejected this is also resolved/rejected
  // with the same data
  bind: function (promise) {
    var that = this;
    promise.then(function (res) {
      that.resolve(res);
    }, function (err) {
      that.reject(err);
    })
  }
};

Як бачите, він досить зріс.


1
можливо, було б добре пов’язати CommonJS Promises / Пропозиція ... для тих, хто зрозумів і хоче заглибитися в цю схему :)
gustavohenke

1
Дякую. Додано посилання на всі технічні характеристики.
Kaizo

2
Ваш приклад абсолютно не працює. Ні стан обіцянки не врегульовується (ваш може бути заповнений і відхилений одночасно), ні додавання зворотних викликів після вирішення проблем, ні thenповернення іншого обіцянки (що є суттєвим).
Бергі,

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

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

7

Q є дуже складною бібліотекою обіцянок з точки зору реалізації, оскільки вона спрямована на підтримку сценаріїв конвеєризації та типу RPC. У мене є моя власна дуже гола реалізація специфікації Promises / A + тут .

В принципі це досить просто. Перш ніж обіцянка буде врегульована / вирішена, ви ведете запис будь-яких зворотних дзвінків або помилок, натискаючи їх у масив. Коли обіцянка врегульована, ви зателефонуєте до відповідних зворотних викликів або помилок і запишете, з яким результатом обіцянка була погашена (і чи була вона виконана чи відхилена). Після того, як це буде врегульовано, ви просто зателефонуєте зворотним дзвінкам або помилкам із збереженим результатом.

Це дає вам приблизно семантику done. Для побудови thenвам просто потрібно повернути нову обіцянку, яка вирішується в результаті виклику зворотних викликів / помилок.

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


2
Ого, ця стаття @KrisKowal - це приголомшливо. Він повинен відправити його у відповідь , щоб отримати десятки upvotes :-)
Берги

1
Дійсно, це чудово, я планую переформатувати його як належну веб-сторінку, щоб вона дещо краще була відформатована.
ForbesLindesay

+1 для статті KrisKowal. Чудове читання.
Дерек Чіанг

посилання, здається, порушено ...
TJ,

6

Як зазначає Форбс у своїй відповіді, я хронізував багато дизайнерських рішень, пов’язаних із створенням такої бібліотеки, як Q, тут https://github.com/kriskowal/q/tree/v1/design . Досить сказати, що існують рівні обіцяної бібліотеки та багато бібліотек, які зупиняються на різних рівнях.

На першому рівні, охопленому специфікацією Promises / A +, обіцянка є проксі-сервером остаточного результату і підходить для управління “локальною асинхронією” . Тобто він підходить для забезпечення того, щоб робота відбувалася в правильному порядку, а також для того, щоб просто і прямо слухати результат операції, незалежно від того, чи вона вже врегульована, або відбудеться в майбутньому. Це також робить так само простим для однієї чи багатьох сторін підписатися на кінцевий результат.

Q, оскільки я його реалізував, пропонує обіцянки, які є проксі-серверами для можливих, віддалених або кінцевих + віддалених результатів. З цією метою його конструкція інвертована, з різними реалізаціями для обіцянок - відкладеними обіцянками, виконаними обіцянками, відхиленими обіцянками та обіцянками для віддалених об’єктів (остання реалізована в Q-Connection). Всі вони мають однаковий інтерфейс і працюють, надсилаючи та отримуючи повідомлення типу "тоді" (чого достатньо для Promises / A +), а також "отримувати" та "викликати". Отже, Q стосується «розподіленої асинхронії» і існує на іншому рівні.

Однак насправді Q було знято з вищого рівня, де обіцянки використовуються для управління розподіленою асинхронією серед взаємно підозрілих сторін, таких як ви, купець, банк, Facebook, уряд - не вороги, можливо, навіть друзі, але іноді з конфліктами інтерес. Q, який я реалізував, розроблений для сумісності API із загартованими обіцянками безпеки (що є причиною відокремлення promiseта resolve), з надією, що він ознайомить людей з обіцянками, навчить їх користуватися цим API і дозволить їм взяти свій код з ними, якщо їм у майбутньому потрібно буде використовувати обіцянки в безпечних компромісах.

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

Все це говорить, немає виправдання тому, що його важко читати. Ми з Доменіком працюємо над версією Q, яка буде більш модульною та доступною, з деякими її відволікаючими залежностями та робочими методами, переміщеними в інші модулі та пакети. На щастя, такі люди, як Forbes , Crockford та інші, заповнили прогалину в освіті, створивши простіші бібліотеки.


посилання, здається, порушено ...
TJ

4

Спочатку переконайтеся, що ви розумієте, як мають працювати Обіцянки. Погляньте на пропозиції CommonJs Promises та специфікацію Promises / A + для цього.

Є дві основні концепції, кожна з яких може бути реалізована у декілька простих рядків:

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

    function Deferred() {
        var callbacks = [], // list of callbacks
            result; // the resolve arguments or undefined until they're available
        this.resolve = function() {
            if (result) return; // if already settled, abort
            result = arguments; // settle the result
            for (var c;c=callbacks.shift();) // execute stored callbacks
                c.apply(null, result);
        });
        // create Promise interface with a function to add callbacks:
        this.promise = new Promise(function add(c) {
            if (result) // when results are available
                c.apply(null, result); // call it immediately
            else
                callbacks.push(c); // put it on the list to be executed later
        });
    }
    // just an interface for inheritance
    function Promise(add) {
        this.addCallback = add;
    }
    
  • Обіцянки мають thenметод, який дозволяє їх прив’язувати. Я приймаю зворотний виклик і повертаю нову обіцянку, яка буде вирішена в результаті цього зворотного виклику після того, як вона була викликана з результатом першої обіцянки. Якщо зворотний виклик повертає Promise, він буде засвоєний, а не вкладений.

    Promise.prototype.then = function(fn) {
        var dfd = new Deferred(); // create a new result Deferred
        this.addCallback(function() { // when `this` resolves…
            // execute the callback with the results
            var result = fn.apply(null, arguments);
            // check whether it returned a promise
            if (result instanceof Promise)
                result.addCallback(dfd.resolve); // then hook the resolution on it
            else
                dfd.resolve(result); // resolve the new promise immediately 
            });
        });
        // and return the new Promise
        return dfd.promise;
    };
    

Подальші концепції полягали б у підтримці окремого стану помилок (з додатковим зворотним викликом для нього) та лові винятків у обробниках, або гарантуванні асинхронності зворотних викликів. Додавши їх, ви отримаєте повністю функціональну реалізацію Promise.

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

function Deferred() {
    var callbacks = [], // list of callbacks
        errbacks = [], // list of errbacks
        value, // the fulfill arguments or undefined until they're available
        reason; // the error arguments or undefined until they're available
    this.fulfill = function() {
        if (reason || value) return false; // can't change state
        value = arguments; // settle the result
        for (var c;c=callbacks.shift();)
            c.apply(null, value);
        errbacks.length = 0; // clear stored errbacks
    });
    this.reject = function() {
        if (value || reason) return false; // can't change state
        reason = arguments; // settle the errror
        for (var c;c=errbacks.shift();)
            c.apply(null, reason);
        callbacks.length = 0; // clear stored callbacks
    });
    this.promise = new Promise(function add(c) {
        if (reason) return; // nothing to do
        if (value)
            c.apply(null, value);
        else
            callbacks.push(c);
    }, function add(c) {
        if (value) return; // nothing to do
        if (reason)
            c.apply(null, reason);
        else
            errbacks.push(c);
    });
}
function Promise(addC, addE) {
    this.addCallback = addC;
    this.addErrback = addE;
}
Promise.prototype.then = function(fn, err) {
    var dfd = new Deferred();
    this.addCallback(function() { // when `this` is fulfilled…
        try {
            var result = fn.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was thrown
            dfd.reject(e);
        }
    });
    this.addErrback(err ? function() { // when `this` is rejected…
        try {
            var result = err.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was re-thrown
            dfd.reject(e);
        }
    } : dfd.reject); // when no `err` handler is passed then just propagate
    return dfd.promise;
};

Не пояснюючи, як це реалізовано, і не показуючи реалізацію відстрочки / обіцянки.
Kaizo

@Kaizo: Дякую, не зрозумів, що OP запитує конкретно про відстрочку. Пояснення додано та переключено на інтерфейс відкладеного. Щось важливе все ще бракує?
Бергі

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

@Kaizo: Так, я навмисно це пропустив, тому що при стислому написанні код лише стає ще більш складним і менш зрозумілим :-) Перевірте мою редакцію ...
Бергі,

У якому випадку addCallbackметод у Promiseкласі буде викликаний більше одного разу? thenМетод просто повертає новий Promiseекземпляр, так чому б зберегти масив зворотних викликів в Deferredкласі?
буксирування

1

Можливо, ви захочете перевірити публікацію в блозі на Adehun.

Adehun - надзвичайно легка реалізація (близько 166 LOC) і дуже корисна для вивчення того, як реалізувати специфікацію Promise / A +.

Застереження : Я написав публікацію в блозі, але ця публікація пояснює все про Адехун.

Функція переходу - Вратар державного переходу

Функція воротаря; гарантує, що переходи стану відбуваються, коли виконуються всі необхідні умови.

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

Функція процесу здійснює правильну дію на основі переходу (наприклад, очікує на виконання) і пояснюється пізніше.

function transition (state, value) {
  if (this.state === state ||
    this.state !== validStates.PENDING ||
    !isValidState(state)) {
      return;
    }

  this.value = value;
  this.state = state;
  this.process();
}

Функція Тоді

Тодішня функція бере два необов'язкові аргументи (обробники onFulfill та onReject) і повинна повернути нову обіцянку. Дві основні вимоги:

  1. Базова обіцянка (та, на яку потім викликається) повинна створити нову обіцянку, використовуючи передані обробники; база також зберігає внутрішнє посилання на цю створену обіцянку, щоб її можна було викликати після виконання / відхилення базової обіцянки.

  2. Якщо базове обіцянку врегульовано (тобто виконано або відхилено), тоді слід негайно викликати відповідного обробника. Adehun.js обробляє цей сценарій, викликаючи процес у тодішній функції.

``

function then(onFulfilled, onRejected) {
    var queuedPromise = new Adehun();
    if (Utils.isFunction(onFulfilled)) {
        queuedPromise.handlers.fulfill = onFulfilled;
    }

    if (Utils.isFunction(onRejected)) {
        queuedPromise.handlers.reject = onRejected;
    }

    this.queue.push(queuedPromise);
    this.process();

    return queuedPromise;
}`

Функція Process - Обробка переходів

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

Процес запускає процедуру вирішення обіцянок на всіх внутрішньо збережених обіцянках (тобто тих, які були приєднані до базової обіцянки за допомогою тодішньої функції) та забезпечує виконання таких вимог Promise / A +:

  1. Асинхронний виклик обробників за допомогою помічника Utils.runAsync (тонка обгортка навколо setTimeout (setImmediate також буде працювати)).

  2. Створення резервних обробників для обробників onSuccess та onReject, якщо вони відсутні.

  3. Вибір правильної функції обробника на основі стану обіцянки, наприклад виконаний або відхилений.

  4. Застосування обробника до значення базової обіцянки. Значення цієї операції передається функції Resolve для завершення циклу обробки обіцянок.

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

    функція process () {var that = this, fillFallBack = function (value) {return value; }, rejectFallBack = функція (причина) {кинути причину; };

    if (this.state === validStates.PENDING) {
        return;
    }
    
    Utils.runAsync(function() {
        while (that.queue.length) {
            var queuedP = that.queue.shift(),
                handler = null,
                value;
    
            if (that.state === validStates.FULFILLED) {
                handler = queuedP.handlers.fulfill ||
                    fulfillFallBack;
            }
            if (that.state === validStates.REJECTED) {
                handler = queuedP.handlers.reject ||
                    rejectFallBack;
            }
    
            try {
                value = handler(that.value);
            } catch (e) {
                queuedP.reject(e);
                continue;
            }
    
            Resolve(queuedP, value);
        }
    });
    

    }

Функція Resolve - Resolving Promises

Це, мабуть, найважливіша частина реалізації обіцянок, оскільки вона обробляє вирішення обіцянок. Він приймає два параметри - обіцянку та значення її роздільної здатності.

Хоча існує безліч перевірок різних можливих значень роздільної здатності; цікавих сценаріїв розв’язання два - це ті, що передбачають передачу обіцянки, та тенабель (об’єкт із тодішнім значенням).

  1. Передаючи значення Promise

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

  1. Передаючи тенабельне значення

Фішка тут полягає в тому, що тоді функцію thenable value потрібно викликати лише один раз (вдале використання для обгортки з функціонального програмування). Так само, якщо отримання тодішньої функції викликає виняток, обіцянку слід негайно відхилити.

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

function Resolve(promise, x) {
  if (promise === x) {
    var msg = "Promise can't be value";
    promise.reject(new TypeError(msg));
  }
  else if (Utils.isPromise(x)) {
    if (x.state === validStates.PENDING){
      x.then(function (val) {
        Resolve(promise, val);
      }, function (reason) {
        promise.reject(reason);
      });
    } else {
      promise.transition(x.state, x.value);
    }
  }
  else if (Utils.isObject(x) ||
           Utils.isFunction(x)) {
    var called = false,
        thenHandler;

    try {
      thenHandler = x.then;

      if (Utils.isFunction(thenHandler)){
        thenHandler.call(x,
          function (y) {
            if (!called) {
              Resolve(promise, y);
              called = true;
            }
          }, function (r) {
            if (!called) {
              promise.reject(r);
              called = true;
            }
       });
     } else {
       promise.fulfill(x);
       called = true;
     }
   } catch (e) {
     if (!called) {
       promise.reject(e);
       called = true;
     }
   }
 }
 else {
   promise.fulfill(x);
 }
}

Конструктор обіцянок

І це все, що поєднує все. Функції виконувати та відхиляти - це синтаксичний цукор, який передає функції відмови для розв’язання та відхилення.

var Adehun = function (fn) {
 var that = this;

 this.value = null;
 this.state = validStates.PENDING;
 this.queue = [];
 this.handlers = {
   fulfill : null,
   reject : null
 };

 if (fn) {
   fn(function (value) {
     Resolve(that, value);
   }, function (reason) {
     that.reject(reason);
   });
 }
};

Сподіваюся, це допомогло пролити більше світла на те, як обіцяють працювати.


Будь ласка, опублікуйте вміст блогу у своїй відповіді або, принаймні, суть його. Посилання не є відповіддю .
Бергі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.