Как реализуется библиотека обещаний/отсрочек?

Как реализована библиотека обещаний/отложенных, например q? Я пытался прочитать исходный код, но мне было трудно понять его, поэтому я подумал, что было бы здорово, если бы кто-то мог объяснить мне с высокого уровня, какие методы используются для реализации promises в однопоточном JS, такие как Node и браузеры.

Ответы

Ответ 1

Мне сложно объяснить, чем показать пример, поэтому здесь очень простая реализация того, что может быть отсрочкой/обещанием.

Отказ от ответственности: Это не функциональная реализация, и некоторые части спецификации Promise/A отсутствуют, это просто объяснение основы promises.

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

Promise:

Сначала нам нужно создать объект обещания с массивом обратных вызовов. Я начну работать с объектами, потому что это яснее:

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);
    }
  }
}

Перенести

Теперь создайте объект отсрочки, который будет иметь обещание:

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);

Подробнее о спецификациях: Спецификация Promise CommonJS. Обратите внимание, что основные библиотеки (Q, when.js, rsvp.js, node -promise,...) следуют спецификации Promises/A.

Надеюсь, я был достаточно ясным.

Изменить:

Как уже было сказано в комментариях, я добавил две вещи в этой версии:

  • Возможность вызова тогда обещания, независимо от того, какой статус он имеет.
  • Возможность подключения promises.

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

Чтобы иметь возможность связывать promises, вам нужно сгенерировать новый отложенный для каждого вызова 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);
    })
  }
};

Как вы можете видеть, он вырос совсем немного.

Ответ 2

Q - очень сложная библиотека обещаний с точки зрения реализации, поскольку она направлена ​​на поддержку сценариев конвейерной обработки и RPC-типа. У меня есть моя собственная очень простая реализация Promises/A + спецификация .

В принципе это довольно просто. Перед тем, как обещание будет разрешено/разрешено, вы сохраняете запись любых обратных вызовов или ошибок, нажимая их в массив. Когда обещание будет разрешено, вы вызываете соответствующие обратные вызовы или errbacks и записываете, в каком результате обещание было урегулировано (и было ли выполнено или отклонено). После этого вы просто вызываете обратные вызовы или errbacks с сохраненным результатом.

Это дает вам примерно семантику done. Чтобы построить then, вам просто нужно вернуть новое обещание, которое разрешено с результатом вызова обратных вызовов /errbacks.

Если вас интересует полное объяснение причин, лежащих в основе разработки полной реализации обещаний с поддержкой RPC и конвейерной обработки, такой как Q, вы можете прочитать кривословное обоснование здесь. Это действительно хороший градуированный подход, который я не могу рекомендовать достаточно высоко, если вы думаете о внедрении promises. Это, вероятно, стоит прочитать, даже если вы собираетесь использовать библиотеку обещаний.

Ответ 3

Как упоминает Forbes в своем ответе, я описал многие дизайнерские решения, связанные с созданием библиотеки типа Q, здесь https://github.com/kriskowal/q/tree/v1/design. Достаточно сказать, что существуют уровни библиотеки обещаний и множество библиотек, которые останавливаются на разных уровнях.

На первом уровне, захваченном спецификацией Promises/A +, обещание является прокси для конечного результата и подходит для управления "локальной асинхронностью" . То есть он подходит для обеспечения того, чтобы работа выполнялась в правильном порядке и для обеспечения простого и прямого прослушивания результата операции независимо от того, была ли она уже решена или произойдет в будущем. Это также делает так же просто, чтобы одна или несколько сторон подписались на конечный результат.

Q, как я его реализовал, предоставляет promises, которые являются прокси-серверами для возможных, удаленных или возможных + удаленных результатов. С этой целью его конструкция инвертируется с различными реализациями для promises -deferred promises, выполнена promises, отклоняется promises и promises для удаленных объектов (последнее реализовано в Q-Connection). Все они имеют один и тот же интерфейс и работают, отправляя и получая сообщения типа "then" (чего достаточно для Promises/A +), но также "get" и "invoke". Итак, Q - это "распределенная асинхронность" и существует на другом уровне.

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

Конечно, есть компромиссы, когда вы двигаетесь вверх по слоям, обычно в скорости. Таким образом, реализация promises также может быть разработана для совместного сосуществования. Здесь вводится понятие "thenable" . Библиотеки Promise на каждом уровне могут быть спроектированы так, чтобы потреблять promises из любого другого уровня, поэтому многие реализации могут сосуществовать, и пользователи могут покупать только то, что им нужно.

Все это сказало, нет никакого оправдания тому, что его трудно читать. Доменик и я работаем над версией Q, которая будет более модульной и доступной, причем некоторые из ее отвлекающих зависимостей и рабочих элементов перемещаются в другие модули и пакеты. К счастью, людям нравится Forbes, Crockford, а другие заполнили образовательный пробел, сделав более простые библиотеки.

Ответ 4

Сначала убедитесь, что вы понимаете, как должны работать Promises. Посмотрите предложения 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;
    }
    
  • Promises имеют метод 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;
};

Ответ 5

Возможно, вы захотите проверить сообщение в блоге в Adehun.

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

Отказ от ответственности: я написал сообщение в блоге, но сообщение в блоге объясняет все об Adehun.

Функция перехода - гейткипер для перехода состояния

Функция гейткипера; гарантирует, что переход состояния произойдет, когда будут выполнены все требуемые условия.

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

Функция процесса выполняет правильное действие на основе перехода (например, ожидает выполнения) и объясняется позже.

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

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

Функция Then

Затем функция принимает два необязательных аргумента (обработчики onFulfill и onReject) и должна возвращать новое обещание. Два основных требования:

  • Базовое обещание (то, на которое тогда вызвано) должно создать новое обещание с использованием переданных в обработчиках; база также хранит внутреннюю ссылку на это созданное обещание, поэтому его можно вызвать, как только обещание базы будет выполнено/отклонено.

  • Если базовое обещание разрешено (то есть выполнено или отклонено), тогда соответствующий обработчик должен быть вызван немедленно. Adehun.js обрабатывает этот сценарий, вызывая процесс в функции then.

``

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;
}`

Функция процесса - переходы обработки

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

Процесс запускает процедуру разрешения Promise во всех встроенных promises (т.е. те, которые были присоединены к базе, обещают через функцию then) и применяют следующие требования Promise/A +:

  • Вызов асинхронных обработчиков с помощью помощника Utils.runAsync(тонкая оболочка вокруг setTimeout (setImmediate также будет работать)).

  • Создание резервных обработчиков для обработчиков onSuccess и onReject, если они отсутствуют.

  • Выбор правильной функции обработчика на основе состояния обещания, например. выполнено или отклонено.

  • Применение обработчика к базовому обещанию. Значение этой операции передается функции Resolve для завершения цикла обработки обещаний.

  • Если возникает ошибка, то прилагаемое обещание немедленно отклоняется.

    function process() {   var that = this,       executeFallBack = функция (значение) {           возвращаемое значение;       },       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 - разрешение Promises

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

Хотя существует множество проверок на различные возможные значения разрешения; интересные сценарии разрешения - два - те, которые связаны с передаваемым обещанием и последующим (объект с тогдашним значением).

  • Передача значения Promise

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

  1. Передача в текущее значение

Ловушка здесь заключается в том, что thenable значения then function должны быть вызваны только один раз (хорошее использование для однократной оболочки из функционального программирования). Аналогично, если извлечение функции then вызывает исключение, обещание немедленно отклоняется.

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

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);
 }
}

Конструктор обещаний

И это тот, который объединяет все это. Функции выполнения и отклонения - это синтаксический сахар, который пропускает функции no-op для разрешения и отклонения.

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);
   });
 }
};

Надеюсь, это помогло пролить свет на то, как работает promises.