This article is the first in a series of wheels. We will start from scratch to write a promise that meets the Promises/A+specification. This series of articles will select some of the more classic front-end wheels for source code analysis and gradually implement them from scratch. This series will learn Promises/A+, Redux, react-redux, vue, dom-diff, webpack, babel, kao, express, asyn. C/await, jquery, Lodash, requirejs, lib-flexible and other front-end classic wheel implementation methods, each chapter of the source code is hosted on github, welcome attention~
Related articles:
Learn to Make Wheels Together (1): Write a Promises/A+compliant promise from scratch
Learn to Make Wheels Together (2): Write a small, complete Redux from scratch
This series of github warehouses:
Learn to make wheel series github together (welcome star ~)
Preface
Promise is a solution for asynchronous programming, which is more reasonable and powerful than traditional solution callback functions and events. It was first proposed and implemented by the community. ES6 wrote it into the language standard, unified its usage, and provided the Promise object natively. This article does not focus on the use of promise. As for the usage, you can see the Promise section in Teacher Ruan Yifeng's ECMAScript 6 series:
This article mainly explains how to implement promise's features and functions step by step from scratch, and ultimately make it conform to Promises/A+, because the explanation is more detailed, so the article is slightly longer.
In addition, each step of the project source code is on github, you can refer to the reference, each step has the corresponding project code and test code, if you like, welcome to a star.~
Project address: The github repository of the code in this article
start
The examples of asynchronous operation used in promise in this paper are all using the fs.readFile method in node, and the setTimeout method can be used to simulate asynchronous operation on browser side.
I. Basic Version
target
- You can create promise object instances.
- The asynchronous method passed in by the promise instance executes the successful callback function of registration when it succeeds, and the failed callback function of registration when it fails.
Realization
function MyPromise(fn) { let self = this; // Caching the current promise instance self.value = null; //Value at Success self.error = null; //Reasons for failure self.onFulfilled = null; //Successful callback function self.onRejected = null; //Failed callback function function resolve(value) { self.value = value; self.onFulfilled(self.value);//Perform a successful callback when resolve } function reject(error) { self.error = error; self.onRejected(self.error)//Failed callback on reject } fn(resolve, reject); } MyPromise.prototype.then = function(onFulfilled, onRejected) { //Register successful and failed callbacks for promise instances here this.onFulfilled = onFulfilled; this.onRejected = onRejected; } module.exports = MyPromise
The code is very short and the logic is very clear. The successful callback and failure callback of this promise instance are registered in the then. When promise reslove, the asynchronous execution result is assigned to the value of promise instance, and the value is passed to the successful callback for execution. Failure assigns the cause of asynchronous execution failure to the error of promise instance, and the value is passed to the failed callback and executed.
This section code
II. Supporting synchronization tasks
We know that when we use promise of es6, we can pass in an asynchronous task or a synchronous task, but our base code above does not support synchronous tasks. If we write this way, we will report errors:
let promise = new Promise((resolve, reject) => { resolve("Synchronized task execution") });
Why? Because it is a synchronous task, when our promise instance reslove, its then method has not been implemented, so the callback function has not been registered, at this time, the successful callback in reslove will surely be wrong.
target
Make promise support synchronization methods
Realization
function resolve(value) { //Use the setTimeout feature to put the specific execution after the setTimeout(() => { self.value = value; self.onFulfilled(self.value) }) } function reject(error) { setTimeout(() => { self.error = error; self.onRejected(self.error) }) }
The simple implementation is to wrap it in reslove and reject with setTimeout and make it execute after the then method is executed, so that promise supports the incoming synchronization method, which is also explicitly required in the Promise/A + specification.
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
This section code
Support synchronization task code
3. Supporting three states
We know that when using promise, promise has three states: pending, fulfilled and rejected. Only the result of an asynchronous operation can determine which state it is currently, and no other operation can change that state. In addition, once the state of promise changes, it will not change again. At any time, we can get the result that the state of promise object changes. There are only two possibilities: from pending to fulfilled and from pending to rejected. As long as these two situations occur, the state solidifies and does not change any more. This result will be maintained all the time. If the change has occurred, you can add a callback function to the promise object and get the result immediately.
target
- Implement three states of promise.
- There are only two possibilities to change the state of promise objects: from pending to fulfilled and from pending to rejected.
- Once the promise state changes, the result will be obtained immediately by adding a callback function to the promise object.
Realization
//Define three states const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; function MyPromise(fn) { let self = this; self.value = null; self.error = null; self.status = PENDING; self.onFulfilled = null; self.onRejected = null; function resolve(value) { //If the state is pending, then modify the state to fulfilled and execute the success logic if (self.status === PENDING) { setTimeout(function() { self.status = FULFILLED; self.value = value; self.onFulfilled(self.value); }) } } function reject(error) { //If the state is pending, then modify the state to rejected and execute the failure logic if (self.status === PENDING) { setTimeout(function() { self.status = REJECTED; self.error = error; self.onRejected(self.error); }) } } fn(resolve, reject); } MyPromise.prototype.then = function(onFulfilled, onRejected) { if (this.status === PENDING) { this.onFulfilled = onFulfilled; this.onRejected = onRejected; } else if (this.status === FULFILLED) { //If the state is fulfilled, the successful callback is executed directly and the success value is passed in. onFulfilled(this.value) } else { //If the state is rejected, the failure callback is executed directly and the failure reason is passed in. onRejected(this.error) } return this; } module.exports = MyPromise
First, we set up three states: pending, fulfilled and rejected. Then we make judgments in reslove and reject. Only when the state is pending, we change the state of promise and perform corresponding actions. In addition, we judge in the then that if the promise has become fulfilled or rejected, we immediately perform its callback and pass the results in.
This section code
IV. Supporting Chain Operation
We usually write promise s as a set of corresponding process operations, such as:
promise.then(f1).then(f2).then(f3)
However, our previous version can only register a callback at most, and we will implement the chain operation in this section.
target
Make promise support chain operation
Realization
To support chain operations, it's very simple. First, when storing callbacks, you need to use arrays instead.
self.onFulfilledCallbacks = []; self.onRejectedCallbacks = [];
Of course, when a callback is executed, it is also necessary to traverse the callback array to execute the callback function.
self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
Finally, the then method needs to be changed, just add a return this to the last line, which is consistent with the principle of jQuery chain operation. Every time the method is invoked, it returns its own instance. The latter method is also the method of the instance, so it can continue to execute.
MyPromise.prototype.then = function(onFulfilled, onRejected) { if (this.status === PENDING) { this.onFulfilledCallbacks.push(onFulfilled); this.onRejectedCallbacks.push(onRejected); } else if (this.status === FULFILLED) { onFulfilled(this.value) } else { onRejected(this.error) } return this; }
This section code
V. Supporting Serial Asynchronous Tasks
In the last section, we implemented chain invocation, but at present only synchronous tasks can be passed in the then method, but we usually use promise, which is generally asynchronous tasks, because we mainly use promise to solve a set of process asynchronous operations. After obtaining user id by the following invocation interface, the user balance can be obtained according to the user id invocation interface, and the user id and asynchronous operation can be obtained. Getting user balances requires calling interfaces, so they are all asynchronous tasks. How can promise support serial asynchronous operations?
getUserId() .then(getUserBalanceById) .then(function (balance) { // do sth }, function (error) { console.log(error); });
target
Enable promise to support serial asynchronous operation
Realization
Here we introduce a common scenario for convenience: read the contents of the file in promise order, and the scenario code is as follows:
let p = new Promise((resolve, reject) => { fs.readFile('../file/1.txt', "utf8", function(err, data) { err ? reject(err) : resolve(data) }); }); let f1 = function(data) { console.log(data) return new Promise((resolve, reject) => { fs.readFile('../file/2.txt', "utf8", function(err, data) { err ? reject(err) : resolve(data) }); }); } let f2 = function(data) { console.log(data) return new Promise((resolve, reject) => { fs.readFile('../file/3.txt', "utf8", function(err, data) { err ? reject(err) : resolve(data) }); }); } let f3 = function(data) { console.log(data); } let errorLog = function(error) { console.log(error) } p.then(f1).then(f2).then(f3).catch(errorLog) //It will output in turn. //this is 1.txt //this is 2.txt //this is 3.txt
In the above scenario, we read 1.txt and print 1.txt content, then read 2.txt and print 2.txt content, then read 3.txt and print 3.txt content, and read files are all asynchronous operations, so we all return a promise. The promise we implemented in the previous section can implement subsequent callbacks after the asynchronous operations are completed, but the callbacks in this section read the contents of the file operation and It's not synchronous, but asynchronous, so after reading 1.txt, when it calls back f1, f2, f3 in onFulfilled Callbacks, the asynchronous operation is not complete, so we wanted to get such output:
this is 1.txt this is 2.txt this is 3.txt
But it actually outputs.
this is 1.txt this is 1.txt this is 1.txt
So in order to realize asynchronous operation serialization, we can't register callback functions in onFulfilled Callbacks of initial promises, but each callback function in onFulfilled Callbacks of corresponding asynchronous operation promises. For example, in the case of reading files, F1 should be in onFulfilled Callbacks of p, and f2 should be in onFulfilled Promised Callbacks of f1. In illedCallbacks, because only in this way can we read 2.txt before printing the result of 2.txt.
However, we usually write promises as follows: promise.then(f1).then(f2).then(f3). We specify all the processes at the beginning, instead of registering F1 callbacks in F1 and F2 callbacks in f2.
How can we maintain this chain writing while enabling asynchronous operations to be implemented? In fact, we let the then method return a new promise instead of its own instance. We can call it bridgePromise. Its greatest function is to link up subsequent operations. Let's look at the specific implementation code.
MyPromise.prototype.then = function(onFulfilled, onRejected) { const self = this; let bridgePromise; //Prevent users from not passing success or failure callbacks, so success or failure callbacks are given to default callbacks. onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : error => { throw error }; if (self.status === FULFILLED) { return bridgePromise = new MyPromise((resolve, reject) => { setTimeout(() => { try { let x = onFulfilled(self.value); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); }) } if (self.status === REJECTED) { return bridgePromise = new MyPromise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(self.error); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); }); } if (self.status === PENDING) { return bridgePromise = new MyPromise((resolve, reject) => { self.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); self.onRejectedCallbacks.push((error) => { try { let x = onRejected(error); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); }); } } //The catch method is actually a grammatical sugar, which is the then method that only passes on Rejected but not on Fulfilled. MyPromise.prototype.catch = function(onRejected) { return this.then(null, onRejected); } //The return value x, which is used to parse the callback function, may be either a normal value or a promise object function resolvePromise(bridgePromise, x, resolve, reject) { //If x is a promise if (x instanceof MyPromise) { //If the promise is pending, continue resolvePromise in its then method to parse its results until the return value is not a promise in pending state. if (x.status === PENDING) { x.then(y => { resolvePromise(bridgePromise, y, resolve, reject); }, error => { reject(error); }); } else { x.then(resolve, reject); } //If x is a normal value, let the bridge Promise state fulfilled and pass it on } else { resolve(x); } }
First, in order to prevent the user from passing in a successful callback function or a failed callback function, we give the default callback function, and then, regardless of the current promise state, we return a bridge Promise to link up the subsequent operations.
In addition, when the callback function is executed, because the callback function may return either an asynchronous promise or a synchronous result, we trust the result of the callback function directly to the bridge Promise and use the resolvePromise method to parse the result of the callback function. If the callback function returns a promise and the state is pending, it is on the n side of the promise. If the value is still pending promise, it will continue to parse until it is not an asynchronous promise, but a normal value. The bridge promise reslove method is used to change the status of bridge Promise to fulfilled, and the onFulfilled Callbacks callback array method is called to pass the value in, so the asynchronous operation will join. It's connected.
It's Abstract here. Let's draw a picture of the scene read in file order to explain the process.
When p.then(f1).then(f2).then(f3):
- Executing p.then(f1) returns a bridge Promise (p2) and puts a callback function in the onFulfilled Callbacks callback list of P. The callback function is responsible for executing F1 and updating the status of p2.
- Then, when. then(f2) returns a bridge Promise (p3), notice that actually p2.then(f2), because p.then(f1) returns p2. At this point, a callback function is placed in the onFulfilled Callbacks callback list of p2, which is responsible for executing F2 and updating the status of p3.
- Then, when. then(f3), a bridge Promise (p4) is returned, and a callback function is placed in the onFulfilled Callbacks callback list of p3, which executes F3 and updates the status of p4.
At this point, the callback relationship is registered, as shown in the figure:
- then, after a period of time, the asynchronous operation in P is finished, the content of 1.txt is read, the callback function of P is executed, the callback function executes f1, the content of 1.txt is printed out "this is 1.txt", and the return value of F1 is put into resolvePromise to start parsing. When resolvePromise sees a promise object coming in, promise is asynchronous, so we have to wait. So we continue resolvePromise as a promise object in the n method of this promise object. It is not a promise object, but a specific value "this is 2.txt". So we call the resolvePromise method of bridgePromise(p2) to change the status of bridgePromise(p2). The new is fulfilled and "this is 2.txt" is passed into the callback function of P 2 to execute.
- The callback of P 2 begins to execute, f2 gets the passed parameter of "this is 2.txt" and starts to execute, prints out the content of 2.txt, and puts the return value of f2 into resolvePromise to start parsing. resolvePromise once it sees that a promise object is passed in, promise is asynchronous, and it has to wait.... The follow-up operation is to repeat 4,5 steps continuously until the end.
So far, we've got the reslove line. Let's look at reject line. reject is actually very simple to handle.
- First, fn and registered callbacks are wrapped in try-catch, and any exception will enter the reject branch.
- Once the code enters the reject branch, it sets the bridge promise to rejected state directly, so it will follow rejected branch. In addition, if the onRejected function of exception handling is not passed, the default is to use throw error to throw the error backwards, which achieves the purpose of error bubbling.
- Finally, a catch function can be implemented to receive errors.
MyPromise.prototype.catch = function(onRejected) { return this.then(null, onRejected); }
At this point, we can happily use promise.then(f1).then(f2).then(f3).catch(errorLog) to read the contents of the file sequentially.
This section code
Support for serial asynchronous task code
VI. Achieving Promises/A+Specification
In fact, by supporting serial asynchronous tasks, we have written promises that are basically complete in function, but not very standardized, such as judgments of other situations, etc. This section will polish our promises compared with the Promises/A + specification. If you just want to learn the core implementation of promise, it doesn't matter if you don't understand this section, because this section does not add the function of promise, but makes promise more standardized and robust.
target
Make promises reach Promises/A+specification and pass the complete test of promises-aplus-tests
Realization
First, let's look at the Promises/A+specification:
Promises/A+Specification Original
Promises/A+Specification Chinese Version
Compared with the previous section of code, the code in this section has not been modified except for a few additional judgments in the resolvePromise function. The complete promise code is as follows:
const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; function MyPromise(fn) { const self = this; self.value = null; self.error = null; self.status = PENDING; self.onFulfilledCallbacks = []; self.onRejectedCallbacks = []; function resolve(value) { if (value instanceof MyPromise) { return value.then(resolve, reject); } if (self.status === PENDING) { setTimeout(() => { self.status = FULFILLED; self.value = value; self.onFulfilledCallbacks.forEach((callback) => callback(self.value)); }, 0) } } function reject(error) { if (self.status === PENDING) { setTimeout(function() { self.status = REJECTED; self.error = error; self.onRejectedCallbacks.forEach((callback) => callback(self.error)); }, 0) } } try { fn(resolve, reject); } catch (e) { reject(e); } } function resolvePromise(bridgepromise, x, resolve, reject) { //2.3.1 Specification to avoid circular references if (bridgepromise === x) { return reject(new TypeError('Circular reference')); } let called = false; //This judgment branch can actually be deleted and replaced by the following branch, because promise is also a thenable object. if (x instanceof MyPromise) { if (x.status === PENDING) { x.then(y => { resolvePromise(bridgepromise, y, resolve, reject); }, error => { reject(error); }); } else { x.then(resolve, reject); } // 2.3.3 Specification, if x is an object or function } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) { try { // Is it the nable object (object/function with the then method)? //2.3.3.1 assigns the n to x.then let then = x.then; if (typeof then === 'function') { //2.3.3.3 If the then is a function, call the then function with x as this, and the first parameter is resolvePromise, and the second parameter is rejectPromise. then.call(x, y => { if (called) return; called = true; resolvePromise(bridgepromise, y, resolve, reject); }, error => { if (called) return; called = true; reject(error); }) } else { //2.3.3.4 If the n is not a function, fulfill promise with x as its value. resolve(x); } } catch (e) { //2.3.3.2 If an exception is thrown when an x.then value is taken, promise is rejected for this exception. if (called) return; called = true; reject(e); } } else { resolve(x); } } MyPromise.prototype.then = function(onFulfilled, onRejected) { const self = this; let bridgePromise; onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : error => { throw error }; if (self.status === FULFILLED) { return bridgePromise = new MyPromise((resolve, reject) => { setTimeout(() => { try { let x = onFulfilled(self.value); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }, 0); }) } if (self.status === REJECTED) { return bridgePromise = new MyPromise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(self.error); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); } if (self.status === PENDING) { return bridgePromise = new MyPromise((resolve, reject) => { self.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); self.onRejectedCallbacks.push((error) => { try { let x = onRejected(error); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); }); } } MyPromise.prototype.catch = function(onRejected) { return this.then(null, onRejected); } // Code needed to execute test cases MyPromise.deferred = function() { let defer = {}; defer.promise = new MyPromise((resolve, reject) => { defer.resolve = resolve; defer.reject = reject; }); return defer; } try { module.exports = MyPromise } catch (e) {}
We can run the test first, we need to install the test plug-in, then execute the test, and pay attention to adding the last few lines of code above to execute the test case.
1.npm i -g promises-aplus-tests 2.promises-aplus-tests mypromise.js
Running the test case shows that the promise code we wrote above passed the full Promises/A+specification test.
Sprinkle flowers and be happy
Then we start to analyze the code in this section. We mainly add two additional judgments in resolvePromise. The first one is that when x and bridge Promise point to the same value, they report the error of circular reference, so that promise conforms to the 2.3.1 specification. Then we add a judgment of X as object or function. This judgment corresponds mainly to the 2.3.3 specification. The Chinese specification is as follows:
This standard corresponds to the nable object, what is the nable object, as long as there is the method of the nable object, then we can implement it according to the specification.
else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) { try { // Is it the nable object (object/function with the then method)? //2.3.3.1 assigns the n to x.then let then = x.then; if (typeof then === 'function') { //2.3.3.3 If the then is a function, call the then function with x as this, and the first parameter is resolvePromise, and the second parameter is rejectPromise. then.call(x, y => { if (called) return; called = true; resolvePromise(bridgepromise, y, resolve, reject); }, error => { if (called) return; called = true; reject(error); }) } else { //2.3.3.4 If the n is not a function, fulfill promise with x as its value. resolve(x); } } catch (e) { //2.3.3.2 If an exception is thrown when an x.then value is taken, promise is rejected for this exception. if (called) return; called = true; reject(e); } }
After writing the branch code, we can actually delete the branch if (x instance of MyPromise) {} because promise is also a thenable object, which can be completely replaced by compatible code. In addition, many duplicate codes in this section can be encapsulated and optimized, but in order to see clearly, there is no abstract encapsulation. If you think there are too many duplicate codes, you can abstract encapsulation by yourself.
This section code
Achieve Promises/A+Specification Code
all, race, resolve, reject methods for promise implementation
In the previous section, we implemented a promise that conforms to the Promises/A+specification. In this section, we implemented some common methods in es6 promise.
target
all, race, resolve, reject methods to implement es6 promise
Realization
We will continue to write on the basis of the preceding:
MyPromise.all = function(promises) { return new MyPromise(function(resolve, reject) { let result = []; let count = 0; for (let i = 0; i < promises.length; i++) { promises[i].then(function(data) { result[i] = data; if (++count == promises.length) { resolve(result); } }, function(error) { reject(error); }); } }); } MyPromise.race = function(promises) { return new MyPromise(function(resolve, reject) { for (let i = 0; i < promises.length; i++) { promises[i].then(function(data) { resolve(data); }, function(error) { reject(error); }); } }); } MyPromise.resolve = function(value) { return new MyPromise(resolve => { resolve(value); }); } MyPromise.reject = function(error) { return new MyPromise((resolve, reject) => { reject(error); }); }
The principle of all is to return a promise. In this promise, all callbacks are registered in the then method of all incoming promises. When the callback is successful, the value is put into the result array. When all callbacks are successful, the returned promises are sent to reslove and the result array is returned to rac. E and all are much the same, but it will not wait for all promises to succeed, but who will quickly return who, the logic of resolve and reject is also simple, you can see.
This section code
Implementation of all, race, resolve, reject method code
8. Implementation of promiseify method
In fact, by the end of the previous section, the promise method has been exhausted. This section describes the promiseify method in bluebird, a famous promise library, because this method is very common and has been asked in previous interviews. What role does promiseify play? Its function is to convert the asynchronous callback function api into promise form. For example, after implementing promiseify on fs.readFile, you can directly use promise to call the method of reading files. Is it very powerful?
let Promise = require('./bluebird'); let fs = require("fs"); var readFile = Promise.promisify(fs.readFile); readFile("1.txt", "utf8").then(function(data) { console.log(data); })
target
promiseify method for bluebird implementation
Realization
MyPromise.promisify = function(fn) { return function() { var args = Array.from(arguments); return new MyPromise(function(resolve, reject) { fn.apply(null, args.concat(function(err) { err ? reject(err) : resolve(arguments[1]) })); }) } }
Although the method is very powerful, it is not difficult to implement. If you want to call promise directly outside, you will return a promise. Inside, you will stitch a callback function parameter after the original parameter. In the callback function, you will execute the promise's reslove method to pass the result out, and promiseify will be realized.
This section code
Implementation of promiseify method
Last
Unknowingly wrote so much, if you think you can give a compliment, and each section of the code is hosted on github, you can look at that section of promise implementation code and test code, also by the way, a star.~
Project address: The github repository of the code in this article
In addition, the implementation of a promise conforming to Promises/A+specification is not only a way of implementation in this paper, but also a relatively easy-to-understand way of implementation as an illustration. You can use your own way to achieve a promise conforming to Promises/A+specification.