I. Why do we need Promise
- “callback hell”
Asynchronous call is the core of JavaScript across browser and Node.js.
JavsScript runtime thread use
callback
to execute result of asynchronous task handled by other threads (Ajax thread, Timer thread, Rendering Thread)Example below are callback hell , which is a headache for programmer. For
1
2
3
4
5
6
7
8// when the nesting level exceeds 3, people will get crazy!!
getData(function(x){ // getData of x
getMoreData(x, function(y){ // getMoreData of y, using x
getMoreData(y, function(z){ // getMoreData of x, using y
...
});
});
});Is there a way to flatten the nesting callbacks, in a chainable way? =>
Node
- Consider the
Node
data structure, it contains child node in itself, similar to our requirement!! Promise
is such a stateful utility object, which can be chained to implement nested callbacks
- Consider the
II. Promise Usage
executer:
execute asynchronous action, generating result
when create new Promise, need to pass in pre-defined executer
1
2
3
4
5
6
7
8
9// executer: a function with parameters: `resolve` and `reject`
let p = new Promise( function(resolve, reject){
ajaxGet( url, function(res){
if(res.status == 200)
resolve(res.data)
else
reject(res.error)
})
})
callbacks in
then
:onFulfilled
&onRejected
specify how to handle the result of asynchronous actionuse case 1: multiple callbacks on a promise
1
2
3
4
5// Assume executor will: resolve(1)
let callback1 = function(res){ console.log("This is from callback1: " + res*1) }
let callback2 = function(res){ console.log("This is from callback2: " + res*2)}
p.then(callback1) // This is from callback1: 1
p.then(callback2) // This is from callback1: 2use case 2: chaining callbacks
1
2
3
4
5// Assume executor will: resolve(1)
let callback1 = function(res){ return res+1 }
let callback2 = function(res){ return res+1 }
let callback3 = function(res){ console.log("The final res will be 3: " + res) }
p.then(callback1).then(callback1).then(callback3) // The final res will be 3: 3
III. Promise: stateful utility object
“Stateful”
Promise is an object, its properties are stateful, ie. property value can change dynamially
state can only change in 2 ways:
pending => fulfilled
,pending => rejected
the cause of stateful is: asynchronous actions inside the executor. Once instantiated to promise object, the executor will be executed inside promise object, and will update
state
,value
,consumers
1
2
3
4
5
6
7function MyPromise( executor ){
this.state = "pending" // keep record of the state update
this.value = undefined // store the result of asynchronous action
this.consumers = [] // place to store callbacks (actually child promises)
executor(this.resolve.bind(this), this.reject.bind(this)) // run executor
}
resolve
&reject
: hooks to receive asynchronous result.- The asynchronous function
executor
, will pass its result to Promise using its hooks:resolve
andreject
- The asynchronous function
then
: hooks to receive passed in callbacksonFulfilled
&onRejected
- the callbacks are passed to Promise using
then
hook - Promise will take care of executing callback, when asynchronous result is ready
- the callbacks are passed to Promise using
How to it chainable
- In fact,
then()
method can not only receive callbacks, - but also can return a new Promise, which can be chained by using another
then
, …., and so on then()
create a new Promise, then put the callbacks inside the newly created Promise, then put into theconsumers
array to consume the asynchronous result when its ready
- In fact,
VI. Write One!
Promise state:
There are only 2 state transitions allowed: from pending to fulfilled, and from pending to rejected. No other transition should be possible, and once a transition has been performed, the promise value (or rejection reason) should not change.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function MyPromise(executor) {
this.state = 'pending';
this.value = undefined;
executor(this.resolve.bind(this), this.reject.bind(this));
}
// provide only two ways to transition
MyPromise.prototype.resolve = function (value) {
if (this.state !== 'pending') return; // cannot transition anymore
this.state = 'fulfilled'; // can transition
this.value = value; // must have a value
}
MyPromise.prototype.reject = function (reason) {
if (this.state !== 'pending') return; // cannot transition anymore
this.state = 'rejected'; // can transition
this.value = reason; // must have a reason
}then
method: the core of chaining[Note 1]: each
then()
will return a new Promise!The
then
method is the core to Promise, which can be chained usingthen
. It has two tasks:- receive callbacks, and store them for later use when asynchorous result is ready
- create new promise, to make promise chainable
In fact, we put callbacks of the current promise, into the newly created child promises. Each callback will correspond a new Promise creation
This way, we can also easily consume the
outcome
of thecallback(result)
, toresolve(outcome)
for the child promises[Note 2]:
- child promise execute its parent’s callback ;
- and resolve outcome of that callback, pass the outcome to promise created by next
then
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// For each then(), create a new Promise to embody this callback pair
MyPromise.prototype.then = function(onFulfilled, onRejected) {
var consumer = new MyPromise(function () {});
// ignore onFulfilled/onRejected if not a function
consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;
// put the consumer with parent's callbacks onto `this.consumers`
this.consumers.push(consumer);
// It's possible that the promise was already resolved,
// so, try to consume the resolved result
this.broadcast();
// must return a promise to make it chainable
// used by next potential `then()` in the chain
return consumer;
};broadcast()
: Consume the asynchorous resultAfter the asynchronous action return a result, we need to
first, update the state to
fulfilled
orrejected
execute the each callback of
onFulfilled
,onRejected
(in child promise) collected bythen
onFulfilled
,onRejected
must be called asynchronouslyfor example,
new Promise(executor).then(callback)
, callback execution must happen after executor. Thus, we can correctly get result even if executor is synchorous
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32MyPromise.prototype.broadcast = function() {
var promise = this;
// only get called after promise is resolved
if (this.state === 'pending') return;
// callback must be executed by its corresponding promise
var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';
// when no callback specified in then(), still need to resolve/reject it
var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';
// onFulfilled/onRejected must be called asynchronously
setTimeout(function() {
// traverse in order & call only once, using `splice(0)` for deletion
promise.consumers.splice(0).forEach(function(consumer) {
try {
var callback = consumer[callbackName];
// if a functon, call callback as plain function
// else, ignore callback
if (callback) {
// child promise resolve outcome of callback(value)
consumer.resolve(callback(promise.value));
} else {
// child promise resolve value without callback
consumer[resolver](promise.value);
}
} catch (e) {
consumer.reject(e);
};
})
}, 0 );
}[Note 2]: the current
then
can only handle chainable promise when callback is pure function; Cannot handle when callback return a new Promise.
Make
then
can response another PromiseThis time, we need to modify the
resolve
method to be able to receive anotherPromise
; At the same time, the original one we call itfulfill
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54// The Promise Resolution Procedure: will treat values that are thenables/promises
// and will eventually call either fulfill or reject/throw.
MyPromise.prototype.resolve = function(x) {
var wasCalled, then;
// 2.3.1
if (this === x) {
throw new TypeError('Circular reference: promise value is promise itself');
}
// 2.3.2
if (x instanceof MyPromise) {
// 2.3.2.1, 2.3.2.2, 2.3.2.3
x.then(this.resolve.bind(this), this.reject.bind(this));
} else if (x === Object(x)) { // 2.3.3
try {
// 2.3.3.1
then = x.then;
if (typeof then === 'function') {
// 2.3.3.3
then.call(x, function resolve(y) {
// 2.3.3.3.3 don't allow multiple calls
if (wasCalled) return;
wasCalled = true;
// 2.3.3.3.1 recurse
this.resolve(y);
}.bind(this), function reject(reasonY) {
// 2.3.3.3.3 don't allow multiple calls
if (wasCalled) return;
wasCalled = true;
// 2.3.3.3.2
this.reject(reasonY);
}.bind(this));
} else {
// 2.3.3.4
this.fulfill(x);
}
} catch(e) {
// 2.3.3.3.4.1 ignore if call was made
if (wasCalled) return;
// 2.3.3.2 or 2.3.3.3.4.2
this.reject(e);
}
} else {
// 2.3.4
this.fulfill(x);
}
}
// 2.1.1.1: provide only two ways to transition
MyPromise.prototype.fulfill = function (value) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'fulfilled'; // 2.1.1.1: can transition
this.value = value; // 2.1.2.2: must have a value
this.broadcast();
}
V. Test Our Own Promise (stack overflow)
1 | function MyPromise(executor) { |
Credits to:
- https://stackoverflow.com/questions/23772801/basic-javascript-promise-implementation-attempt/42057900#42057900
- https://www.promisejs.org/implementing/
- https://javascript.info/promise-chaining
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- http://thecodebarbarian.com/write-your-own-node-js-promise-library-from-scratch.html