Movatterモバイル変換


[0]ホーム

URL:


/ #JavaScript

How JavaScript promises actually work from the inside out

How JavaScript promises actually work from the inside out

By Shailesh Shekhawat

One of the most important questions I faced in interviews was how promises are implemented. Since async/await is becoming more popular, you need to understand promises.

What is a Promise?

A promise is an object which represents the result of an asynchronous operation which is either resolved or rejected (with a reason).

There are 3 states

  • Fulfilled:onFulfilled() will be called (e.g.,resolve() was called)
  • Rejected:onRejected() will be called (e.g.,reject() was called)
  • Pending: not yet fulfilled or rejected

So let’s see how’s it is implemented:

https://github.com/then/promise/blob/master/src/core.js

According to the definition atMozilla: It takes anexecutor function as an argument.

functionnoop(){}functionPromise(executor){if (typeofthis !=='object') {thrownewTypeError('Promises must be constructed via new');  }if (typeof executor !=='function') {thrownewTypeError('Promise constructor\'s argument is not a function'); }this._deferredState =0;this._state =0;this._value =null;this._deferreds =null;if (executor === noop)return;  doResolve(executor,this);}

Looks like a simple function with some properties initialized to0 ornull. Here are a few things to notice:

**this._state** property can have three possible values as described above:

0 - pending1 - fulfilledwith _value2 - rejectedwith _value3 - adopted the stateof another promise, _value

Its value is0 (pending) when you create a newpromise.

LaterdoResolve(executor, this) is invoked withexecutor and promise object.

Let’s move on to the definition ofdoResolve and see how it’s implemented.

/*** Take a potentially misbehaving resolver function and make sure* onFulfilled and onRejected are only called once.** Makes no guarantees about asynchrony.*/functiondoResolve(fn, promise){var done =false;var resolveCallback =function(value){if (done)return;      done =true;      resolve(promise, value); };var rejectCallback =function(reason){if (done)return;   done =true;   reject(promise, reason);};var res = tryCallTwo(fn, resolveCallback, rejectCallback);if (!done && res === IS_ERROR) {    done =true;    reject(promise, LAST_ERROR); }}

Here it is again callingtryCallTwo function with executor and 2 callbacks. The callbacks are again callingresolve andreject

Thedone variable is used here to make sure the promise is resolved or rejected only once, so if you try to reject or resolve a promise more than once then it will return becausedone = true.

functiontryCallTwo(fn, a, b){try {    fn(a, b);   }catch (ex) {     LAST_ERROR = ex;return IS_ERROR;  }}

This function indirectly calls the mainexecutor callback with 2 arguments. These arguments contain logic on howresolve orreject should be called. You can checkresolveCallback andrejectCallback indoResolve function above.

If there is an error during execution it will store the error inLAST_ERROR and return the error.

Before we jump to theresolve function definition, let’s check out the.then function first:

Promise.prototype.then =function(onFulfilled, onRejected){if (this.constructor !==Promise) {return safeThen(this, onFulfilled, onRejected);   }var res =newPromise(noop);   handle(this,new Handler(onFulfilled, onRejected, res));return res;};functionHandler(onFulfilled, onRejected, promise){this.onFulfilled =typeof onFulfilled ==="function" ? onFulfilled  :null;this.onRejected =typeof onRejected ==="function" ? onRejected :null;this.promise = promise;}

So in the above function, then is creating newpromise and assigning it as a property to a new function calledHandler. TheHandler function has argumentsonFulfilled andonRejected. Later it will use this promise to resolve or reject with value/reason.

As you can see, the.then function is calling again another function:

handle(this,new Handler(onFulfilled, onRejected, res));

Implementation:

functionhandle(self, deferred){while (self._state ===3) {    self = self._value;  }if (Promise._onHandle) {Promise._onHandle(self);  }if (self._state ===0) {if (self._deferredState ===0) {         self._deferredState =1;         self._deferreds = deferred;return;    }if (self._deferredState ===1) {       self._deferredState =2;       self._deferreds = [self._deferreds, deferred];return;    }    self._deferreds.push(deferred);return; }   handleResolved(self, deferred);}
  • There is a while loop which will keep assigning the resolved promise object to the current promise which is also a promise for_state === 3
  • If_state = 0(pending) and promise state has been deferred until another nested promise is resolved, its callback is stored inself._deferreds
functionhandleResolved(self, deferred){   asap(function(){// asap is external lib used to execute cb immediatelyvar cb = self._state ===1 ? deferred.onFulfilled :     deferred.onRejected;if (cb ===null) {if (self._state ===1) {           resolve(deferred.promise, self._value);       }else {         reject(deferred.promise, self._value);       }return;  }var ret = tryCallOne(cb, self._value);if (ret === IS_ERROR) {       reject(deferred.promise, LAST_ERROR);    }else {      resolve(deferred.promise, ret);    }  });}

What's happening:

  • If the state is 1(fulfilled) then call theresolve elsereject
  • IfonFulfilled oronRejected isnull or if we used an empty.then()resolved orreject will be called respectively
  • Ifcb is not empty then it is calling another functiontryCallOne(cb, self._value)
functiontryCallOne(fn, a){try {return fn(a);   }catch (ex) {      LAST_ERROR = ex;return IS_ERROR;   }} a) {

**tryCallOne**: This function only calls the callback that is passed into the argumentself._value. If there is no error it will resolve the promise, otherwise it will reject it.

Every promise must supply a.then() method with the following signature:

promise.then(  onFulfilled?:Function,  onRejected?:Function) =>Promise
  • BothonFulfilled() andonRejected() are optional.
  • If the arguments supplied are not functions, they must be ignored.
  • onFulfilled() will be called after the promise is fulfilled, with the promise’s value as the first argument.
  • onRejected() will be called after the promise is rejected, with the reason for rejection as the first argument.
  • NeitheronFulfilled() noronRejected() may be called more than once.
  • .then() may be called many times on the same promise. In other words, a promise can be used to aggregate callbacks.
  • .then() must return a new promise.

Promise Chaining

.then should return a promise. That's why we can create a chain of promises like this:

Promise.then(() =>Promise.then(() =>Promise.then(result => result) )).catch(err)

Resolving a promise

Let’s see theresolve function definition that we left earlier before moving on to.then():

functionresolve(self, newValue){// Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedureif (newValue === self) {return reject(        self,newTypeError("A promise cannot be resolved with itself.")     );   }if (      newValue &&     (typeof newValue ==="object" ||typeof newValue ==="function")   ) {var then = getThen(newValue);if (then === IS_ERROR) {return reject(self, LAST_ERROR);   }if (then === self.then && newValueinstanceofPromise) {      self._state =3;     self._value = newValue;     finale(self);return;   }elseif (typeof then ==="function") {      doResolve(then.bind(newValue), self);return;   }}   self._state =1;   self._value = newValue;   finale(self);}
  • We check if the result is a promise or not. If it’s a function, then call that function with value usingdoResolve().
  • If the result is a promise then it will be pushed to thedeferreds array. You can find this logic in thefinale function.

Rejecting a promise:

Promise.prototype['catch'] =function (onRejected){returnthis.then(null, onRejected);};

The above function can be found in./es6-extensions.js.

Whenever we reject a promise, the.catch callback is called which is a sugar coat forthen(null, onRejected).

Here is the basic rough diagram that I have created which is a birds-eye view of what's happening inside:

Image

Let’s see once again how everything is working:

For example, we have this promise:

newPromise((resolve, reject) => {setTimeout(() => {    resolve("Time is out");  },3000)}).then(console.log.bind(null,'Promise is fulfilled')).catch(console.error.bind(null,'Something bad happened: '))
  1. Promiseconstructor is called and an instance is created withnew Promise
  2. executor function is passed todoResolve(executor, this) and callback where we have definedsetTimeout will be called bytryCallTwo(executor, resolveCallback, rejectCallback)so it will take 3 seconds to finish
  3. We are calling.then() over the promise instance so before ourtimeout is completed or any asyncapi returns,Promise.prototype.then will be called as.then(cb, null)
  4. .then creates a newpromise and passes it as an argument tonew Handler(onFulfilled, onRejected, promise)
  5. handle function is called with the originalpromise instance and thehandler instance we created in point 4.
  6. Inside thehandle function, currentself._state = 0 andself._deferredState = 0 soself_deferredState will become1 andhandler instance will be assigned toself.deferreds after that control will return from there
  7. After.then() we are calling.catch() which will internally call.then(null, errorCallback) — again the same steps are repeated frompoint 4 to point 6 and skip point 7 since we called.catch once
  8. Currentpromise state ispending and it will wait until it is resolved or rejected. So in this example, after 3 seconds,setTimeout callback is called and we are resolving this explicitly which will callresolve(value).
  9. resolveCallback will be called with valueTime is out :) and it will call the mainresolve function which will check ifvalue !== null && value == 'object' && value === 'function'
  10. It will fail in our case since we passedstring andself._state will become1 withself._value = 'Time is out' and laterfinale(self) is called.
  11. finale will callhandle(self, self.deferreds) once becauseself._deferredState = 1, and for the chain of promises, it will callhandle() for eachdeferred function.
  12. In thehandle function, sincepromise is resolved already, it will callhandleResolved(self, deferred)
  13. handleResolved function will check if_state === 1 and assigncb = deferred.onFulfilled which is ourthen callback. LatertryCallOne(cb, self._value) will call that callback and we get the final result. While doing this if any error occurred thenpromise will be rejected.

When a promise is rejected

In this case, all the steps will remain the same — but inpoint 8 we callreject(reason). This will indirectly callrejectCallback defined indoResolve() andself._state will become2. In thefinale functioncb will be equal todeferred.onRejected which will be called later bytryCallOne. That’s how the.catch callback will be called.

That's all for now! I hope you enjoyed the article and it helps in your next JavaScript interview.

If you encounter any problem feel free toget in touchor comment below. I would be happy to help ?

Don’t hesitate to clap if you considered this a worthwhile read!

Originally published at101node.io on February 05, 2019.


If you read this far, thank the author to show them you care.

Learn to code for free. freeCodeCamp's open source curriculum has helped more than 40,000 people get jobs as developers.Get started

ADVERTISEMENT

[8]ページ先頭

©2009-2025 Movatter.jp