Interpret Promise

What is Promise

Official definition: A Promise is an object representing the eventual completion or failure of an asynchronous operation. To be brief, it makes possible to schedule callbacks elegantly, like an assembly line.
myPromise.then(action1).then(action2).then(action3).catch(handleRejection)

A Promise could be in one of three states:

  1. pending: initial state, neither fulfilled or rejected.
  2. fulfilled: operation was completed successfully.
  3. rejected: operation failed.

How Promise works

Constructor

Syntax:

1
2
3
4
5
6
7
8
new Promise(executor)

// executor:
function(resolve, rejec) {
// some asynchronous operations
}

// `resolve` and `reject` are functions whose names do not matter.

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// new Promise(executor)
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 300);
});

promise1.then((value) => {
console.log(value);
// expected output: "foo"
});

console.log(promise1);
// expected output: [object Promise]

At first I was confused since it appears that resolve passed to constructor has some association with Promise.then method. But this is wrong: they own no ties when executing. resolve in constructor is immediately called(sync) when creating a new Promise object, whereas handler passed to Promise.then might be called later(async). executor, or resolve/reject, is used to set the state of Promise, whereas the handler passed to Promise.then will react to the state of Promise [1]

Promise.then() and Chaining

Syntax:

1
2
3
4
5
6
7
p.then(onFulfilled[, onRejected]);

p.then(value => {
// fulfillment
}, reason => {
// rejection
});

Demo:

1
2
3
4
5
6
7
8
const promise1 = new Promise((resolve, reject) => {
resolve("Success!");
});

promise1.then((value) => {
console.log(value);
// expected output: "Success!"
});

Promise.then() is the most important part. It takes up two functions. onFulfilled will be called when the Promise is fulfilled, take the fulfillment value from the Promise, and return a new Promise which can be chained. onRejected will be called if Promise is rejected. In the demo, string Success! is the fulfillment value of promise1, and is printed when onFulfilled is called in .then(). For more details, see MDN docs.

Chaining is one essential usage of Promise. See an example:

1
2
3
4
5
6
7
8
9
10
p.then(action1).then(action2).then(action3).then(action4).catch(errHandler);
/*
(((((Promise), PromiseA), PromiseB), PromiseC), PromiseD)

(((((Promise)-> action1) -> action2) -> action3) -> action4)
((((PromiseA) -> action2) -> action3) -> action4)
(((PromiseB) -> action3) -> action4)
((PromiseC) -> action4)
(PromiseD)
*/

The promises of a chain are initialized as a first-in-first-out queue: tasks enqueued first are excuted first. When a promise/task is handled, it gets popped, and a new generated promise gets enqueued.

Promise.resolve()

Syntax:

1
Promise.resolve(value);

Demo:

1
2
3
4
5
6
7
8
Promise.resolve("Success").then(
function (value) {
console.log(value); // "Success"
},
function (value) {
// not called
}
);

Promise.resolve() returns a resolved Promise with given value. If value is a promise, it will be returned. If value is a “thenable” object, the returned promise will keep the “thenable”. Otherwise return a promise fulfilled with the value. MDN docs gives more details.

Implementation

Now let’s implement one simple Promise. Do not intent to design a perfect one which strictly follow the Promises/A+ standard, but aim for better interpretation.

Constructor

Constructor receives parameter executor, and executor receives resolve/reject function which will be immediately executed. reolve/reject will change the state of promise. onFulfilledQueue/onRejectedQueue stores asynchronous onFulfilled/onRejected operations from Promise.prototype.then() as promise is in pending state, and will be executed as long as promise’s state become fulfilled/rejected.

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

var Promise = function (executor) {
this.state = "pending";
this.onFulfilledQueue = [];
this.onRejectedQueue = [];
var _this = this;
function resolve(value) {
if (_this.state === PENDING) {
_this.state = FULFILLED;
_this.value = value;
_this.onFulfilledQueue.forEach((fn) => fn(value));
}
}
function reject(reason) {
if ((_this.state = PENDING)) {
_this.state = REJECTED;
_this.reason = reason;
_this.onRejectedQueue.forEach((fn) => fn(reason));
}
}
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
};

Promise.prototype.then()

Promise.prototype.then() appears more complicated. It is a state machine, returning a new promise given current promise’s state.

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
56
57
58
59
60
Promise.prototype.then = function (onFulfilled, onRejected) {
// a. dealing with case: p.then('a').then(1, eor('e')), where onFulfilled & onRejected are not function
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function"
? onRejected
: (reason) => {
throw reason;
};
var _this = this;
var promise2 = new Promise((resolve, reject) => {
if (_this.state === FULFILLED) {
// b. mimic microtask / async operation
setTimeout(() => {
try {
let x = onFulfilled(_this.value);
// resolve returned value
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
} else if (_this.state === REJECTED) {
// mimic microtask / async operation
setTimeout(() => {
try {
let x = onRejected(_this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
} else if (_this.state === PENDING) {
// c. push unexcuted tasks to onFulfilled/onRejected queue.
// setTimeout mimic microtask
_this.onFulfilledQueue.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(_this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
_this.onRejectedQueue.push(() => {
setTimeout(() => {
try {
let x = onRejected(_this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
});
return promise2;
};

a. The two lines deal with case when onFulfilled/onRejected are not functions. If an object gets passed, Promise.ptorotype.then() will simple return this object.

1
2
3
4
5
6
7
8
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function"
? onRejected
: (err) => {
throw err;
};

b. Here setTimeout() mimics microtask. For how promise microtask works, see here

c. As a promise keep pending state, all handlers(tasks) passed to .then() need to be queued. Once the state changed, tasks in onFulFilledQueue and onRejectedQueue are executed in order.

Promise Resolution Procedure

resolvePromise() in Promise.prototype.then() illustrates the promise resolution procedure that is defined here. The code in this part is mainly from a great promise implementation. flag ensures the resolution procedure only run once.

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
var resolvePromise = function (promise2, x, resolve, reject) {
if (promise2 === x) {
reject(new TypeError("Infinite Chaining!"));
return;
}
// Promise/A+ 2.3.3
if ((x && typeof x === "object") || typeof x === "function") {
let flag = false;
try {
let then = x.then;
if (typeof then === "function") {
then.call(
x,
(y) => {
if (flag) return;
resolvePromise(promise2, y, resolve, reject);
flag = true;
},
(r) => {
if (flag) return;
reject(r);
flag = true;
}
);
} else {
if (flag) return;
resolve(x);
flag = true;
}
} catch (e) {
if (flag) return;
reject(e);
flag = true;
}
} else {
resolve(x);
}
};

Conclusion

This post takes me two days. Previously no idea came to me of diving into details of Promise, until last week optimizations were required, then I realized my poverty on Javascript’s asynchronous programming. This post mainly explains the mechanism of Promise, and implements one according Promises/A+ standard. Here is complete code, please install tester first npm install -g promises-aplus-tests, then run promises-aplus-tests promise.js.

Relative Readings

Promise - Javascript MDN

Using Promises - Javascript MDN

细嗅 Promise

Promise 的源码实现(完美符合 Promise/A+规范)

Promises/A+

javascript.info


[[1]](#id01)[stackoverflow](https://stackoverflow.com/questions/31324110/why-does-the-promise-constructor-require-a-function-that-calls-resolve-when-co)