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 | const promise = new Promise((resolve, reject) => { |
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
11const 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 | //example1 |
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 | //example2 |
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 | //example3 |
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 | //example4 |
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 | //example5 |
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
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 | Function noop () {} // empty callback function for then |
Statement
1 | module.exports = Promise; |
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 | function doResolve(fn, promise) { |
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 | function resolve(self, newValue) { |
1 | //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 |
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 | //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 |
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 | function handle(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 | If the future state has not been set (0), then set the callback (deferred) as a separate callback |
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 | 1 Status 3 When waiting for the result of another promise - here the wait will enter |
It can be seen that only when the promise ends or relies on other promises will it enter finale.
1 | function finale(self) { |
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 | function handleResolved(self, deferred) { |
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 | const promise = new Promise((resolve, reject) => { |
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