IT박스

Promise / Defer 라이브러리는 어떻게 구현됩니까?

itboxs 2020. 10. 21. 07:48
반응형

Promise / Defer 라이브러리는 어떻게 구현됩니까?


q 와 같은 promise / defer 라이브러리는 어떻게 구현됩니까? 소스 코드를 읽으려고했지만 이해하기가 꽤 어려웠 기 때문에 누군가가 단일 스레드 JS 환경에서 프라 미스를 구현하는 데 사용되는 기술이 무엇인지 높은 수준에서 설명해 주시면 좋을 것이라고 생각했습니다. Node 및 브라우저와 같습니다.


예제를 보여주는 것보다 설명하기가 더 어렵다는 것을 알았습니다. 그래서 여기에 연기 / 약속이 무엇인지에 대한 매우 간단한 구현이 있습니다.

면책 조항 : 이것은 기능적 구현이 아니며 Promise / A 사양의 일부가 누락되었습니다. 이것은 단지 약속의 기초를 설명하기위한 것입니다.

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

연기 :

이제 promise를 가질 지연 객체를 만듭니다.

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 사양 . 주요 라이브러리 (Q, when.js, rsvp.js, node-promise, ...)는 Promises / A 사양을 따릅니다 .

내가 충분히 명확 했길 바랍니다.

편집하다:

의견에서 질문했듯이이 버전에는 두 가지를 추가했습니다.

  • 어떤 상태를 가지고 있더라도 약속을 호출 할 수있는 가능성.
  • 약속을 연결할 수있는 가능성.

해결 될 때 프라 미스를 호출 할 수 있으려면 프라 미스에 상태를 추가해야하며 그때가 호출되면 해당 상태를 확인해야합니다. 상태가 해결되거나 거부되면 데이터 또는 오류와 함께 콜백을 실행하십시오.

thenPromise 를 연결하려면 각 호출에 대해 새로운 지연을 생성해야 하며, Promise가 해결 / 거부되면 콜백의 결과로 새 Promise를 해결 / 거부해야합니다. 따라서 프라 미스가 완료되면 콜백이 새 프라 미스를 반환하면 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);
    })
  }
};

보시다시피 꽤 성장했습니다.


Q는 파이프 라이닝 및 RPC 유형 시나리오를 지원하는 것을 목표로하기 때문에 구현 측면에서 매우 복잡한 promise 라이브러리입니다. 여기에 Promises / A + 사양 에 대한 나만의 아주 기본적인 구현이 있습니다 .

원칙적으로 아주 간단합니다. promise가 해결 / 해결되기 전에 콜백 또는 errback을 배열로 푸시하여 기록을 유지합니다. Promise가 해결되면 적절한 콜백 또는 errback을 호출하고 Promise가 어떤 결과를 얻었는지 (그리고 그것이 이행되었는지 거부되었는지) 기록합니다. 정산 후 저장된 결과로 콜백 또는 errback을 호출하면됩니다.

그것은 당신에게 done. 빌드하려면 then콜백 / errbacks를 호출 한 결과로 해결 된 새 promise를 반환하면됩니다.

RPC 및 Q와 같은 파이프 라이닝을 지원하는 완전한 약속 구현 개발에 대한 추론에 대한 전체 설명에 관심이 있다면 여기에서 kriskowal의 추론을 읽을 수 있습니다 . 약속을 구현할 생각이라면 충분히 추천 할 수없는 정말 멋진 점진적 접근 방식입니다. 약속 라이브러리를 사용하는 경우에도 읽을만한 가치가 있습니다.


Forbes가 그의 답변에서 언급했듯이 Q와 같은 라이브러리를 만드는 데 관련된 많은 디자인 결정을 기록했습니다 . 여기 https://github.com/kriskowal/q/tree/v1/design . Promise 라이브러리에는 레벨이 있고 다양한 레벨에서 멈추는 많은 라이브러리가 있습니다.

Promises / A + 사양에 의해 캡처 된 첫 번째 수준에서 promise는 최종 결과에 대한 프록시이며 "로컬 비동기" 관리에 적합합니다 . 즉, 작업이 올바른 순서로 이루어 지도록 보장하고 작업이 이미 해결되었는지 또는 앞으로 발생할 것인지에 관계없이 작업 결과를 듣는 것이 간단하고 직관적인지 확인하는 데 적합합니다. 또한 하나 또는 여러 당사자가 최종 결과를 구독하는 것처럼 간단합니다.

Q, 구현 한대로 최종, 원격 또는 최종 + 원격 결과에 대한 프록시 인 약속을 제공합니다. 이를 위해 Promise에 대한 다양한 구현 (지연된 약속, 이행 된 약속, 거부 된 약속 및 원격 객체에 대한 약속 (마지막은 Q-Connection에서 구현 됨))을 사용하여 설계가 반전됩니다. 이들은 모두 동일한 인터페이스를 공유하고 "then"(Promises / A +에 충분 함)과 같은 메시지를주고 받으면서도 "get"및 "invoke"메시지를주고받습니다. 따라서 Q는 "분산 비 동시성" 에 대한 것이며 다른 계층에 존재합니다.

그러나 Q는 실제로 더 높은 계층에서 제거되었습니다. 여기서 약속은 적이 아니라 친구 일 수도 있지만 때로는 충돌이있는 사용자, 상인, 은행, Facebook, 정부와 같은 상호 의심스러운 당사자 간에 분산 된 비 동시성을 관리하는 데 사용됩니다. 관심. 내가 구현 한 Q는 강화 된 보안 약속 ( promise분리의 이유)과 API 호환되도록 설계되었으며 resolve, 사람들에게 약속을 소개하고이 API를 사용하도록 교육하고 코드를 가져갈 수 있기를 바랍니다. 향후 보안 매시업에서 promise를 사용해야하는 경우

물론 일반적으로 속도면에서 레이어를 위로 이동할 때 절충안이 있습니다. 따라서 약속 구현도 공존하도록 설계 할 수 있습니다. 이것이 "thenable" 의 개념이 들어가는 곳입니다. 각 계층의 Promise 라이브러리는 다른 계층의 Promise를 사용하도록 설계 될 수 있으므로 여러 구현이 공존 할 수 있으며 사용자는 필요한 것만 구매할 수 있습니다.

이 모든 말은 읽기 어렵다는 변명의 여지가 없습니다. Domenic과 저는 좀 더 모듈화되고 접근하기 쉬운 Q 버전을 작업 중이며, 일부 산만 한 종속성과 해결 방법은 다른 모듈과 패키지로 옮겨졌습니다. 고맙게도 Forbes , Crockford 및 다른 사람들과 같은 사람들은 더 간단한 라이브러리를 만들어 교육적 격차를 메 웠습니다 .


먼저 Promises가 작동하는 방식을 이해하고 있는지 확인하십시오. CommonJs Promises 제안Promises / A + 사양살펴보십시오 .

몇 가지 간단한 라인으로 각각 구현할 수있는 두 가지 기본 개념이 있습니다.

  • Promise는 결과로 비동기 적으로 해결됩니다. 콜백을 추가하는 것은 투명한 작업입니다. Promise가 이미 해결되었는지 여부와 관계없이 사용 가능하면 결과와 함께 호출됩니다.

    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;
    }
    
  • Promise에는 then연결을 허용 하는 방법이 있습니다. 콜백을 받고 첫 번째 약속의 결과로 호출 된 후 해당 콜백의 결과로 해결 될 새 Promise를 반환합니다. 콜백이 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;
};

Adehun 블로그 게시물 을 확인하는 것이 좋습니다.

Adehun은 매우 가벼운 구현 (약 166 LOC)이며 Promise / A + 사양을 구현하는 방법을 배우는 데 매우 유용합니다.

면책 조항 : 블로그 게시물을 썼지 만 블로그 게시물은 Adehun에 대한 모든 것을 설명합니다.

전환 기능 – 상태 전환을위한 게이트 키퍼

게이트 키퍼 기능; 모든 필수 조건이 충족 될 때 상태 전환이 발생하도록합니다.

조건이 충족되면이 함수는 promise의 상태와 값을 업데이트합니다. 그런 다음 추가 처리를 위해 프로세스 기능을 트리거합니다.

프로세스 기능은 전환 (예 : 이행 보류 중)에 따라 올바른 작업을 수행하며 나중에 설명합니다.

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

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

Then 함수

then 함수는 두 개의 선택적 인수 (onFulfill 및 onReject 핸들러)를 취하며 새 promise를 반환해야합니다. 두 가지 주요 요구 사항 :

  1. 기본 약속 (그때 호출되는 약속)은 전달 된 처리기를 사용하여 새 약속을 만들어야합니다. base는 또한이 생성 된 promise에 대한 내부 참조를 저장하므로 기본 promise가 이행 / 거부되면 호출 할 수 있습니다.

  2. 기본 약속이 해결되면 (즉, 이행 또는 거부) 적절한 핸들러를 즉시 호출해야합니다. 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 함수가 호출 될 때 호출됩니다. 따라서 then 함수에서 호출되었을 수 있으므로 보류중인 promise를 확인해야합니다.

프로세스는 내부적으로 저장된 모든 약속 (즉 then 함수를 통해 기본 약속에 첨부 된 약속)에 대해 약속 해결 절차를 실행하고 다음 Promise / A + 요구 사항을 적용합니다.

  1. Utils.runAsync 도우미를 사용하여 처리기를 비동기 적으로 호출합니다 (setTimeout 주위의 얇은 래퍼 (setImmediate도 작동 함)).

  2. onSuccess 및 onReject 핸들러가 누락 된 경우에 대한 대체 핸들러 작성.

  3. 약속 상태 (예 : 이행 됨 또는 거부 됨)를 기반으로 올바른 핸들러 함수 선택.

  4. 기본 약속의 값에 핸들러를 적용합니다. 이 작업의 값은 Resolve 함수에 전달되어 약속 처리주기를 완료합니다.

  5. 오류가 발생하면 첨부 된 Promise가 즉시 거부됩니다.

    function process () {var that = this, fulfillFallBack = function (value) {return value; }, rejectFallBack = function (reason) {throw reason; };

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

    }

해결 기능 – 약속 해결

이것은 약속 해결을 처리하므로 약속 구현에서 가장 중요한 부분 일 것입니다. 두 가지 매개 변수 인 promise와 해상도 값을받습니다.

다양한 가능한 해상도 값에 대한 많은 검사가 있지만; 흥미로운 해결 시나리오는 전달되는 promise와 thenable (then 값이있는 개체)과 관련된 두 가지 시나리오입니다.

  1. 약속 가치 전달

해결 값이 다른 약속 인 경우 약속은이 해결 값의 상태를 채택해야합니다. 이 해결 값은 보류 중이거나 정산 될 수 있으므로이를 수행하는 가장 쉬운 방법은 새 then 핸들러를 해결 값에 연결하고 그 안의 원래 약속을 처리하는 것입니다. 해결 될 때마다 원래 약속이 해결되거나 거부됩니다.

  1. thenable 값 전달

여기서 catch는 thenable 값의 then 함수가 한 번만 호출되어야한다는 것입니다 (함수 프로그래밍에서 한 번 래퍼를 사용하는 데 유용함). 마찬가지로 then 함수를 검색 할 때 Exception이 발생하면 약속이 즉시 거부됩니다.

이전과 마찬가지로 then 함수는 궁극적으로 promise를 해결하거나 거부하는 함수로 호출되지만 여기서 차이점은 첫 번째 호출에서 설정되고 후속 호출이 작업이되지 않도록하는 호출 된 플래그입니다.

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

The Promise Constructor

And this is the one that puts it all together. The fulfill and reject functions are syntactic sugar that pass no-op functions to resolve and reject.

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

I hope this helped shed more light into the way promises work.

참고URL : https://stackoverflow.com/questions/17718673/how-is-a-promise-defer-library-implemented

반응형