2020-08-12

Promise A+

Disclaimer

This article is originally based on my understanding of this concept. No guarantee on the accuracy.šŸ˜…

[toc]

Promise A+ step by step

https://promisesaplus.com/

A promise represents the eventual result of an asynchronous operation

State Machine

States: ā€˜pendingā€™;ā€™fulfilledā€™;ā€™rejectedā€™;

1
2
3
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

executor

executor will be executed immediately, it takes two function as parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
function Promise(executor){
this.state = PENDING;
function resolve()
function reject()
//executor(resolve, reject);
//user input the executor, which may cause error, wrap it with try and catch
try{
//the executor would synchronously run using the resolve and reject function created in the constructor
executor(resolve, reject);
}catch(error){
reject(error);
}
}

resolve & reject function

these two function are to

  • pass the async results outside for chaining which avoids nested callback
  • change the state of current promise instance;

Only when the state is PENDING, a transition can be made;

1
2
3
4
5
const resolve=(value)=>{
if(this.state === PENDING){
this.state = FULFILLED;
}
}

then

chain revoke => Promise.prototype.then(onRes,onRej)

Methods in then can obtain the value passed from the promise instance => use this =>pass the value/reason out by passing them into the resolve/rejection function

1
2
3
4
5
6
7
8
9
10
Promise.prototype.then = function(onFulfilled, onRejected){
//check if onFuillfilled and onRejected are functions
if(this.state === FULFILLED){ //it must be called after promise is fulfilled, with promiseā€™s value as its first argument.
typeof onFulfilled === 'function' && onFulfilled(this.value);
}
if(this.state === REJECTED){//it must be called after promise is rejected, with promiseā€™s reason as its first argument.
typeof onRejected === 'function' && onRejected(this.reason);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//constructor:
const resolve=(value)=> {
if(this.state === PENDING){
this.state = FULFILLED;
//so the value was attached to the promise instance
//.then can recieve the value
this.value = value;
}
}
const resolve=(reason)=> {
if(this.state === PENDING){
this.state = FULFILLED;
this.reason = reason;
}
}

then() returns a new promise instance

1
2
const promise2 = new Promise((resolve, reject) => {})
return promise2;

the new promise instance, can keep calling then on itself and pass the previous value/reason to the next then by passing it to the resolve and reject functions of itself.

If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.

If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

the result (value/reson) should ā€˜penetrateā€™ to the next then.

Providing a default function: (value)=>value

1
2
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

so when onFulfilled /onRejected are not function , promise2:

1
2
3
4
  //let x=onFulfilled(this.value)==>this.value
if(this.state === FULFILLED){resolve(x)}
//let x = onRejected(this.reason)==>this.reason
if(this.state === REJECTED){reject(x)}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise2 = new Promise((resolve, reject) => {
if(this.state === FULFILLED){ //state of
let x = onFulfilled(this.value);
//for next `then`
resolve(x);
}
if(this.state === REJECTED){
let x = onRejected(this.reason);
reject(x);
}
if(this.state === PENDING){
// TODO
}
})
return promise2;

the onFullfilled(this.value) and onRejected(this.reason) can possibly create error, wrap it with try and catch;

If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).

eg. p.then((v)=>v.data) x==>v.data

Encapsulate the logic in a new function resolvePromise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(this.state === FULFILLED){
try{
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}catch(error){
reject(error);//If onFulfilled throws an exception e, promise2 must be rejected with e as the reason.
}
}
if(this.state === REJECTED){
try{
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(error){
reject(error);//If onRejected throws an exception e, promise2 must be rejected with e as the reason.
}
}

this.state===PENDING

if the promise isnā€™t resolved when the then() has already been invoked, how to obtain the value/reason from the promise after it was fulfilled/rejected to run the onFullfilled/onRejected function?

Store the callbacks in the constructor.

1
2
this.onResolvedCallback = [];
this.onRejectedCallback = [];
1
2
3
4
5
6
7
8
const resolve=(value)=>{
if(this.state === PENDING){
this.state = FULFILLED;
this.value = value;
this.onResolvedCallback.length > 0 &&
this.onResolvedCallback.forEach(fn => fn()); //If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then.
}
}

and add the pending call back in the then() method and introduce the setTimeout to ensure the callback can be invoked asynchronously.

onFulfilled or onRejected must not be called until the execution context stack contains only platform code

In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a ā€œmacro-taskā€ mechanism such as setTimeout or setImmediate, or with a ā€œmicro-taskā€ mechanism such as MutationObserver or process.nextTick.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if(this.state === PENDING){
//pending
this.onResolvedCallback.push(() => {
//use setTimeout to imple the async callback
setTimeout(()=>{
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}catch(error){
reject(error);
}
})
});
this.onRejectedCallback.push(() => {
setTimeout(()=>{
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(error){
reject(error);
}
})
});
}

The Promise Resolution Procedure : resolvePromise()

The promise resolution procedure is an abstract operation taking as input a promise and a value, which we denote as [[Resolve]](promise, x). If x is a thenable, it attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise. Otherwise, it fulfills promise with the value x.

To implement the functionality of the [[Resolve]](promise, x), the resolvePromsie() takes the new promise promise2, the returned value x, and the resolve and reject function of new promise as parameters

If x is a promise, adopt its state

  1. If x is pending, promise must remain pending until x is fulfilled or rejected.
  2. If/when x is fulfilled, fulfil promise with the same value.
  3. If/when x is rejected, reject promise with the same reason.

The promise2 will adopt/wait for x until it was fulfilled/rejected; ā€˜Adoptā€™ means to call the resolve and reject function of its own with the results of promise x.

Theses are the same behaviour of a then method of a promise/thenable object.

If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.

Use a flag called to mark the first called function

Here we treat the promise and thenable object using same function:

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
if(x && typeof x === 'object' || typeof x === 'function'){
let called = false;
try{
let then = x.then; //If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
if(typeof then === 'function'){//If then is a function,
then.call(x, ////call it with x as `this`
y => { //first argument resolvePromise
if(called) return;
called = true;
//If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
resolvePromise(promise2, y, resolve, reject);
},
r => {//second argument rejectPromise
if(called) return;
called = true;
reject(r);//f/when rejectPromise is called with a reason r, reject promise with r.
});
}else{ //If then is not a function, fulfill promise with x.
resolve(x);
}
}
catch(e){//If calling then throws an exception e,
if(called) return;//If resolvePromise or rejectPromise have been called, ignore it.
called = true;
reject(e);//Otherwise, reject promise with e as the reason.
}
}

Avoid dead loop:

If promise and x refer to the same object, reject promise with a TypeError as the reason.

When x === p2, it would cause dead loop: p2.then().then()

1
2
3
if(x === promise2)  {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
}

If x is not an object or function, fulfill promise with x.

1
2
3
else{
resolve(x)
}

The final version of resolvePromise:

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
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2)
return reject(
new TypeError('Chaining cycle detected for promise #<Promise>')
);
if ((x && typeof x === 'object') || typeof x === 'function') {
let called = false;
try {
let then = x.then;
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}

The constructor and prototype of Promise:

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
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function Promise(executor) {
const _this = this;
_this.state = PENDING;
_this.value = void 0;
_this.reason = void 0;
_this.onResolvedCallback = [];
_this.onRejectedCallback = [];

function resolve(value) {
if (_this.state === PENDING) {
_this.state = FULFILLED;
_this.value = value;
_this.onResolvedCallback.length > 0 &&
_this.onResolvedCallback.forEach((fn) => fn());
}
}
function reject(reason) {
if (_this.state === PENDING) {
_this.state = REJECTED;
_this.reason = reason;
_this.onRejectedCallback.length > 0 &&
_this.onRejectedCallback.forEach((fn) => fn());
}
}

try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
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
55
Promise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason;
};
const promise2 = new Promise((resolve, reject) => {
if (this.state === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
}
if (this.state === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
}
if (this.state === PENDING) {
this.onResolvedCallback.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallback.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
}
});
return promise2;
};