Promise Source Code Analysis

This week, I used NodeJs to process pictures in the coding process. Because the interfaces for picture processing are all Promises, my program is full of nested returns of Promises. The result of then returns another Promise, and another Promise. Some of the then in the series will return a new Promise, which once confused me.

After sorting it out, I couldn’t help but be curious about why Promise could be so magical, so I read its source code and found that the design behind some accustomed functions was wonderful.

Promise

ES6 provides Promise constructor function, we create a Promise instance, Promise constructor function receives a function as a parameter, the incoming function has two parameters, namely two functions’resolve ‘and’reject’ The effect is that’resolve ‘will Promise The state changes from unsuccessful to successful, and the result of the asynchronous operation is passed as a parameter; similarly,’ reject 'changes the state from non-failed to failed, and is called when the asynchronous operation fails, and the error reported by the asynchronous operation is passed as a parameter.
After the instance is created, you can use the then method to specify the success or failure of the callback function, which is more beautiful and readable than the nested callback function of f1 (f2 (f3))

When we create a new Promise, we need to pass in a function as a parameter. The passed function has two parameters, namely two functions, resolve and reject. The inside of the function body performs an asynchronous operation, and then according to whether the result of the operation is Call a different function for the desired result. If you get the correct return, call resolve function and take the result you need to return as a parameter. If you don’t get the correct return, call reject function and take the error information as a parameter., so that the outside world can get the error information. Here’s a simple example:

1
2
3
4
5
6
7
8
9
10
11
12
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const num = Math.random();
num > .5 ? resolve(`success:${num}`) : reject(`fail:${num}`);
}, 1000);
});

promise.then((resolveVal) => {
console.log(val);
}).catch(rejectVal => {
console.log(rejectVal)
})

This example creates a Promise that generates a random number after one second. If the random number is greater than 0.5, resolve will be called, and if it is less than 0.5, reject will be called if it is less than 0.5.

When resolve is executed, the state of the Promise will change to resolved and the function passed in then will be executed, which is actually equivalent to the function passed in then being executed as a resolve function. Similarly, when reject is executed, the state of the Promise will change to Rejected, and the function passed in catch will be used as the reject function to execute.

In general, then and catch load two specific functions that need to be executed when resolving and rejecting the Promise.

Then the rules

  • The next input of the’then 'method requires the previous output

  • If a promise is executed and returns a promise, the execution result of this promise will be passed to the next’then '. That is to say, if you return a PromiseB in the then of PromiseA, the result of PromiseB will be used as the imported parameter of the next then of PromiseA.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const promiseA = new Promise((resolve, reject) => {
    ...
    });
    const promiseB = new Promise((resolve, reject) => {

    })
    promiseA.then((resolveA) => {
    return promiseB;
    }).then((resolveB) => {

    })

    In this case, the second then argument is the return result of the promise.

  • If the result returned in’then 'is not a Promise object but a normal value, this result will be regarded as the successful result of the next then

If the current then fails, the next then fails

  • If the return is undefined, the next success will follow regardless of the current success or failure

  • If there is no method written in’then ‘, the value will pass through and be passed to the next’then’

  • return val in the then function is the same as Promise resolve (val)

For example:

1
2
3
4
5
6
//example1
promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve('promise1');
}, 1000)
})

When the argument is a non-promise, the state of the promise immediately changes to resolve after 1 second, and the event inside then is executed.

1
2
3
4
5
6
7
8
9
//example2
promise1 = new Promise((resolve) => {
setTimeout(() => {
promise2 = new Promise((resolve, reject) => {
resolve('promise2');
})
resolve(promise2);
}, 1000)
})

When the parameter is another promise, the state of promise1 is determined by promise2. When promise2 changes the state, the state of promise1 will also change accordingly, and the state will remain the same.

1
2
3
4
5
6
7
//example3
promise1 = new Promise((resolve) => {
resolve('promise1');
})
promise2 = promise1.then((data) => {
return 'promise2';
})

When the callback function directly returns a non-promise, as in example1 above, the current promise2 state changes to resolve. It is equivalent to executing (resolve (‘non-promise’))

1
2
3
4
5
6
7
8
9
10
//example4
promise1 = new Promise((resolve) => {
resolve('promise1');
})
promise2 = promise1.then((data) => {
promise3 = new Promise((resolve, reject) => {
resolve('promise3');
})
return promise3;
})

When the callback function directly returns a promise3, like example2 above, the state of the current promise2 depends on primise3, which is equivalent to executing (resolve (promise3))

1
2
3
4
5
6
7
//example5
promise1 = new Promise((resolve) => {
resolve('promise1');
})
promise2 = promise1.then((data) => {
console.log( iamnotundefined );
})

When the code in the callback function reports an error and is not caught, the current promise state becomes reject. (The asynchronous error code cannot be caught, which will not affect the promise state change)

Rules of catch

  • catch will be called when executing reject
  • catch will be executed when there is any error that has not been handled before

Source code analysis

Promise源码地址

The following comments illustrate the possible values for the Promise state:

0 - Waiting
1 - condition satisfied (value is _value)
2 - Reject condition (value _value)
Adopting the state and value of another promise

Once the state value is not 0,

Before the formal declaration of Promise, in order to reduce the display of try catch in the code, several tool functions were defined.

Tool 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Function noop () {} // empty callback function for then

// States:
//
// 0 - pending
// 1 - fulfilled with _value
// 2 - rejected with _value
// 3 - adopted the state of another promise, _value
//
// once the state is no longer pending (0) it is immutable

// All `_` prefixed properties will be reduced to `_{random number}`
// at build time to obfuscate them and discourage their use.
// We don't use symbols or Object.defineProperty to fully hide them
// because the performance isn't good enough.


// to avoid using try/catch inside critical functions, we
// extract them to here.
var LAST_ERROR = null;
var IS_ERROR = {};
function getThen(obj) {
try {
return obj.then;
} catch (ex) {
LAST_ERROR = ex;
return IS_ERROR;
}
}

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

Statement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = Promise;
function Promise(fn) {
if (typeof this ! 'object') {
throw new TypeError('Promises must be constructed via new');
}
if (typeof fn ! 'function') {
throw new TypeError('Promise constructor\'s argument is not a function');
}
_deferredState = 0;//This state is used to indicate what the future state will be like, only useful if the current Promise state depends on another Promise, that is, resolve a Promise
This._state = 0;//the current state of the Promise
_value = null;//the value of resolve of the current Promise
_deferreds = null;//array of callback functions executed when the _deferredState becomes successful, i.e. greater than 2
if (fn = noop) return;
doResolve(fn, this);
}

A function promise takes a function as its argument and must be created with new.

Initialize _deferredState and _state to 0, _value and _deferreds to null,
If the passed function is an empty function, return directly.
Under normal circumstances, enter doResolve to start the process.

doResolve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function doResolve(fn, promise) {
var done = false;
//Note that fn is what we pass in our new Promise (resolve, reject) = > {if success resolve else reject}, tryCallTwo will pass the second parameter to resolve, and the third parameter to reject, so that when we call resolve in the declared Promise The second parameter of trayCallTwo is actually called.
var res = tryCallTwo(fn, function (value) {
If (done) returns;//prevents running twice
done = true;
resolve(promise, value);
}, function (reason) {
if (done) return;
done = true;
reject(promise, reason);
});
if (!done && res = IS_ERROR) {
done = true;
reject(promise, LAST_ERROR);
}
}

Here, the synchronous direct call to the incoming function, talking about two functions (that is, externally written resolve and reject) as parameters passed in, after the call is completed, check whether it is not completed in the case of an error, if it is directly rejected.
For the incoming resolve function and reject function, after waiting for the result, if it has not yet completed, continue the process through resolve and reject as defined in this document.

** Note that the second and third parameters of the tryCallTwo function are both a function. The parameters of these two functions are actually the two functions ** passed in when we new a new Promise. ** The resolve and reject in this code are the methods defined inside the Promise. **

resolve

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
function resolve(self, newValue) {
//The solution result of a Promise cannot be itself (because according to the principle of then we mentioned at the beginning, if you return a new Promise, the state of the current Promise will depend on the new Promise, and if it depends on itself, then it will always be in a loop and in a pending state)

//The newValue here is actually the parameter resolved in the Promise instance we defined. According to the previous usage method, it can be a string, an array, etc., or another Promise.
if (newValue = self) {
return reject(
self,
new TypeError('A promise cannot be resolved with itself.')
);
}
//when the new value is the parameter of resolve function and the type is object or function
//typeof Promised instance = 'object'
That is to say, the condition for the success of this if is to resolve an object, function or Promise instance
if (
newValue &&
(typeof newValue = 'object' || typeof newValue = 'function')
) {
var then = getThen(newValue); // let then = newValue.then
if (then = IS_ERROR) { //IS_ERROR就是上面所说的工具中声明的,就是一个空对象,只有当return newValue.then发生异常时才会为IS_ERROR
return reject(self, LAST_ERROR);
}
if (
then = self.then &&
newValue instanceof Promise//If the resvole is a Promise, and the then of this Promise is the same as the then of the current Promise (this then is generally the same and is defined on the prototype of the Promise), the result of the current Promise is directly used as the final result.
) {
_state = 3;//state 3 takes another promise as the result
self._value = newValue;
Finale (self);//then take the result of this Promise
return;
} else if (typeof then = 'function') {//走到这里就说明,resolve了一个有then方法的对象(或者一个Promise,并且和当前Promise的then不同,这种情况比较少见)
DoResolve (then.bind (newValue), self);//recursion calls doResolve, we just saw that the first parameter of doResolve is the function passed in when we new Promise, and the second parameter is the pointer of the current Promise instance, That is, when the then function is executed in recursion, the internal this points to the newValue here, and the self has not changed
return;
}
}
//If there is no return above, it will go here. When it comes here, it means that resolve ordinary values, such as numbers and strings, mark completion, and enter the end process.
_state = 1;//Status 1 indicates that you have entered the success state
self._value = newValue;
finale(self);
}

function reject(self, newValue) {
//Set reject status and reason
self._state = 2;
self._value = newValue;
if (Promise._onReject) {
Promise._onReject (self, newValue);//process callback notification
}
Finale (self);//end
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//This function will only be executed when the state of self is not 0. At this time, the execution of handle will execute the second parameter passed in, that is, _deferreds. _deferredState 1 indicates that _deferreds is a Handler, and _deferredState is 2 indicates that _deferreds is an array of Handlers, all executed, and then assign _deferreds to null
//That is to say, finale executes all handlers once
//Each Hander is passed in through the then method. The then method has two parameters, onFulfilled and onRejected, which are the function that should be executed when resolving and the function that should be executed when rejecting.
//If the _state is 3, the finale is executed, in fact, the handler created by the current Promise through then is hung on the _deferreds of the Promise instance on which it depends
function finale(self) {
if (self._deferredState = 1) {
handle(self, self._deferreds);
self._deferreds = null;
}
if (self._deferredState = 2) {
for (var i = 0; i < self._deferreds.length; i++) {
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
}

Then there are _deferredState and _deferreds that have not been touched before, what are they used for?
Before answering these, we first recall that in the Promise, when the promise is created and returned, the following operation is then to get the result (of course, including catch), so let’s take a look at the implementation of this then:

then

1
2
3
4
5
6
7
8
9
//The parameters of then are two functions, onFulfilled is the callback function called when we resolve, and onRejected is the callback function called when we reject
Promise.prototype.then = function(onFulfilled, onRejected) {
if (this.constructor ! Promise) {
return safeThen(this, onFulfilled, onRejected);
}
var res = new Promise(noop);
handle(this, new Handler(onFulfilled, onRejected, res));
return res;
};

The usage of safeThen and then is basically the same. They both create an asynchronous empty callback res, and then use onFulfilled, onRejected and res to create a Handler. Then the core is on the handle 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
28
29
30
31
function handle(self, deferred) {
//self._state = 3说明self指向的Promise实例的状态依赖另一个Promise实例,这个实例就是self._value
That is to say, through this loop, eventually self will eventually point to a state that only depends on its own Promise instance
//That is to say, assuming that self is p1, it depends on the state of p2. When p1 calls handle, and p2 has not resolved, in fact, the Handler created by p1 through then hangs on the _deferreds of p2, and when p2 resolves, execute it in turn.
while (self._state = 3) {
self = self._value;
}
if (Promise._onHandle) { // for injection - not in main loop
Promise._onHandle(self);
}
//If the state only depends on its own Promise instance and there is no result, save the incoming callback function first
//_deferredState 0 indicates that the callback function has not been saved, then assign the callback function to the _deferreds, this time _deferreds just a callback function
//_deferredState 1 indicates that the callback function has been saved, then the callback function and the original callback function stored in the _deferreds are constructed into an array and reassigned to the _deferreds, this time _deferreds just an array of callback functions
//_deferredState 2 indicates that _deferreds is already an array of functions, just push it
//until a certain call handle, the state of self is not 0, will execute handleResolved
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);
}

The parameter of this function is deferred, so it means that deferred is Handler, which means delayed processing. To be clear, it is what needs to be done after completing the promise.
So what is the specific process like?
First, determine whether the current state depends on another promise, if so, wait through while
Then onHandle is just a progress callback provided to the outside, which is ignored here for now
When the state is 0, this is the setting of future processing.

1
2
3
4
If the future state has not been set (0), then set the callback (deferred) as a separate callback
If the future state has been set (1), then set the callback (deferred) into the callback array
If the other state (2 +), then directly into the callback array.
The processing in the case of state 0 is returned here. Because at this time, the promise is executed synchronously and then, the function process for future processing is set.

When the state is non-zero, handleResolved is entered, which should be the place where the processing ends after completion. Wait, the above only enters the handle from then, at that time the promise should not be completed, and the call to complete the processing must be somewhere else. By searching for the call to handle, you can see that it is also in the finale function, so it is connected to the above. Let’s review when the finale will be called?

1
2
3
1 Status 3 When waiting for the result of another promise - here the wait will enter
When 2 state 1 is completed
3 state 2 when rejected

It can be seen that only when the promise ends or relies on other promises will it enter finale.

1
2
3
4
5
6
7
8
9
10
11
12
function finale(self) {
if (self._deferredState = 1) {
handle(self, self._deferreds);
self._deferreds = null;
}
if (self._deferredState = 2) {
for (var i = 0; i < self._deferreds.length; i++) {
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
}

The finale will remove the deffereds placed before one by one, call handle, then the state is non-0, directly enter handleResolved, the code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function handleResolved(self, deferred) {
asap(function() {
//_state is 1, the first parameter of the then function is executed, which is the successful callback function, otherwise the callback function of reject is executed.
var 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);
}
});
}

Here is relatively simple, through asynchronous asap call, if there is no onFulfilled (onRejected failure), then directly call resolve (reject), if there is first call onFulfilled (onRejected failure), according to the result to call resolve (reject).

Wait… Don’t the resolve and reject here appear in the above process? Please note that the resolve and rejected promises here are empty promises created at then, which means that nothing will be executed (directly into the finale no handle case). So what really affects the process here is that for the callback execution of deferred.onFulfilled or deferred.onRejected, after executing the callback, the execution process of this promise is completed.

In summary, the execution process of a promise is like this

Creating a Promise

  • Set the function that needs to be executed, that is, new Promise is the incoming function
  • Set the completed callback, that is, the two functions passed in by then, the first is executed when resolving, and the other is executed when rejecting. If a Promise2 is resolved, the Handler created by then will be mounted on Promise2, and wait until Promise2 resolves and then executes together.
  • Commencing function
  • Select callback based on execution result

Another mention of safeThen, the role of safeThen is to continue to safely execute then when the environment this is no longer a Promise when then is called.

What exactly does Promise mean by microtask?

We read the source code and then go back to understand why the Promise is a microtask

Let’s take the starting code as an example.

1
2
3
4
5
6
7
8
9
10
11
12
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const num = Math.random();
num > .5 ? resolve(`success:${num}`) : reject(`fail:${num}`);
}, 1000);
});

promise.then((resolveVal) => {
console.log(val);
}).catch(rejectVal => {
console.log(rejectVal)
})

In this code, setTimeout This code will be executed directly, and then create a macro task, that is, resolve or reject after 1s, and then the following then and catch actually create two handlers, and these two Handlers will be thrown into the microtask by the asap package after calling resolve or reject.

Therefore, we have been talking about promises as microtasks, which actually means that the incoming functions in then and catch will be put into the microtask queue when the state changes.

Reference article:

https://juejin.im/post/5caf147af265da035d0c698a

https://juejin.im/entry/599968f6518825244630f809

https://www.jianshu.com/p/b63ec30eefdd