_During interviews, Promise is often asked about; some interviewers go a little further and ask if they know how to implement Promise or if they have read the source code of Promise; today we'll see how Promise is implemented internally as a chain call.
_Promise is simply a container that holds the results of an event (usually an asynchronous operation) that will end in the future.Syntax, Promise is an object from which messages for asynchronous operations can be retrieved.Promise provides a unified API, and asynchronous operations can be handled in the same way.
_Promise was previously implemented through callback functions. Callback functions themselves are OK, but nesting levels are too deep and it's easy to fall into callback hell.
const fs = require('fs'); fs.readFile('1.txt', (err,data) => { fs.readFile('2.txt', (err,data) => { fs.readFile('3.txt', (err,data) => { //There may be subsequent code }); }); }); Copy Code
_If logical judgment or exception handling is required after each reading of the file, the entire callback function will be very complex and difficult to maintain.Promise appears to solve this pain point, so we can override the above callback nesting with Promise:
const readFile = function(fileName){ return new Promise((resolve, reject)=>{ fs.readFile(fileName, (err, data)=>{ if(err){ reject(err) } else { resolve(data) } }) }) } readFile('1.txt') .then(data => { return readFile('2.txt'); }).then(data => { return readFile('3.txt'); }).then(data => { //... }); Copy CodePromise specification
_promise was first proposed by the commonjs community, when a number of specifications were proposed.The promise/A specification is more acceptable.However, the promise/A specification is relatively simple, on this basis, people later proposed the promise/A+ specification, that is, the specification actually implemented in the industry; es6 also uses this specification, but es6 also joined in this specification Promise.all,Promise.race,Promise.catch,Promise.resolve,Promise.reject And other methods.
_We can use scripts to test whether the Promise we write meets the promise/A+ specification.Add the Promise we implemented to the following code:
Promise.defer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd; } Copy Code
_and then throughModule.exportsExport, install scripts for tests:
npm install -g promises-aplus-tests Copy Code_Execute the following commands in the directory where Promise is implemented:
promises-aplus-tests promise.js Copy Code_Next, the scripts will test our scripts one by one against the promise/A+ specification.
Promise Basic Structure_Let's review how we use Promise in general:
var p = new Promise(function(resolve, reject){ console.log('implement') setTimeout(function(){ resolve(2) }, 1000) }) p.then(function(res){ console.log('suc',res) },function(err){ console.log('err',err) }) Copy Code
_First of all, Promise instantiates an object through a constructor, then processes the asynchronous returned results through the then method on the instance object.At the same time, the promise/A+ specification specifies:
promise is an object or function that has the then method and behaves in accordance with this specification.
The current state of a Promise must be one of three states: Pending, Fulfilled, and Rejected.
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function Promise(executor) { var _this = this this.state = PENDING; //state this.value = undefined; //Successful results this.reason = undefined; //Reasons for failure function resolve(value) {} function reject(reason) {} } Promise.prototype.then = function (onFulfilled, onRejected) { }; module.exports = Promise; Copy Code
_When we instantiate Promise, the constructor immediately calls the incoming executor, which we can try:
let p = new Promise((resolve, reject) => { console.log('Executed'); }); Copy Code
_Therefore, in Promise the constructor executes immediately, while passing in the resolve and reject functions as parameters:
function Promise(executor) { var _this = this this.state = PENDING; //state this.value = undefined; //Successful results this.reason = undefined; //Reasons for failure function resolve(value) {} function reject(reason) {} executor(resolve, reject) } Copy Code
_But executor may also have exceptions, so try/catch captures them:
try { executor(resolve, reject); } catch (e) { reject(e); } Copy CodeInvariant
The_promise/A+ specification states that when a Promise object has been changed from Pending to Fulfilled or Rejected, it cannot be changed again and the final value cannot be changed.
_Therefore, we judge in the callback functions resolve and reject that the state can only be changed if it is pending:
function resolve(value) { if(_this.state === PENDING){ _this.state = FULFILLED _this.value = value } } function reject(reason) { if(_this.state === PENDING){ _this.state = REJECTED _this.reason = reason } } Copy Code
_We change the state at the same time, save the successful result or the reason for the failure in the callback function in the corresponding attributes for easy access later.
then implementation_When Promise's state changes, whether it succeeds or fails, the then callback function is triggered.Therefore, the implementation of the then is also simple, that is, to call different functions that handle the end values according to the state.
Promise.prototype.then = function (onFulfilled, onRejected) { if(this.state === FULFILLED){ typeof onFulfilled === 'function' && onFulfilled(this.value) } if(this.state === REJECTED){ typeof onRejected === 'function' && onRejected(this.reason) } }; Copy Code
_As stated in the specification, onFulfilled and onRejected are optional, so we make a type of judgment on the two values:
OnFulfilled and onRejected are optional parameters.If onFulfilled is not a function, it must be ignored.If onRejected is not a function, it must be ignored
_Code written here, it seems that there are all possible implementations. Let's write a demo test:
var myP = new Promise(function(resolve, reject){ console.log('implement') setTimeout(function(){ reject(3) }, 1000) }); myP.then(function(res){ console.log(res) },function(err){ console.log(err) }); Copy Code
Goose, unfortunately, when it runs, we find that only the execution in the constructor is printed, and none of the following then functions are executed at all.Let's clean up the code to run smoothly:
_When the function inside the then runs, resolve is asynchronous and has not yet had time to modify the state, so we need to deal with the asynchronous case.
Support Asynchronous_So how do we get our Proise to support asynchronization?We can refer to the publish subscription mode and when executing the then method, if it is still in PENDING state, store the callback function in an array, and when the state changes, remove the callback function from the array; so let's first define the variable in Promise:
function Promise(executor) { this.onFulfilled= []; //Successful callbacks this.onRejected= []; //Failed callbacks } Copy Code_Thus, when the n executes, if it is still in PENDING state, we do not execute the callback function immediately, but store it:
Promise.prototype.then = function (onFulfilled, onRejected) { if(this.state === FULFILLED){ typeof onFulfilled === 'function' && onFulfilled(this.value) } if(this.state === REJECTED){ typeof onRejected === 'function' && onRejected(this.reason) } if(this.state === PENDING){ typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled) typeof onRejected === 'function' && this.onRejected.push(onRejected) } }; Copy Code
_Once stored, it can be called when resolve or reject executes asynchronously:
function resolve(value) { if(_this.state === PENDING){ _this.state = FULFILLED _this.value = value _this.onFulfilled.forEach(fn => fn(value)) } } function reject(reason) { if(_this.state === PENDING){ _this.state = REJECTED _this.reason = reason _this.onRejected.forEach(fn => fn(reason)) } } Copy Code
_Children's shoes may ask why onFulfilled and onRejected on this side need to exist in the array, is it okay to receive them directly with one variable?Here's an example:
var p = new Promise((resolve, reject)=>{ setTimeout(()=>{ resolve(4) }, 0) }) p.then((res)=>{ //4 res console.log(res, 'res') }) p.then((res1)=>{ //4 res1 console.log(res1, 'res1') }) Copy Code
_We called then twice, and if it was a variable, it would surely end up running only the last then, overwriting the previous one. If it was an array, both thens would work correctly.
_So, when we run demo, we can see the results as expected; a simple Promise gasket of about forty lines is finished.Here's the complete code:
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function Promise(executor) { var _this = this this.state = PENDING; //state this.value = undefined; //Successful results this.reason = undefined; //Reasons for failure this.onFulfilled = [];//Successful callbacks this.onRejected = []; //Failed callbacks function resolve(value) { if(_this.state === PENDING){ _this.state = FULFILLED _this.value = value _this.onFulfilled.forEach(fn => fn(value)) } } function reject(reason) { if(_this.state === PENDING){ _this.state = REJECTED _this.reason = reason _this.onRejected.forEach(fn => fn(reason)) } } try { executor(resolve, reject); } catch (e) { reject(e); } } Promise.prototype.then = function (onFulfilled, onRejected) { if(this.state === FULFILLED){ typeof onFulfilled === 'function' && onFulfilled(this.value) } if(this.state === REJECTED){ typeof onRejected === 'function' && onRejected(this.reason) } if(this.state === PENDING){ typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled) typeof onRejected === 'function' && this.onRejected.push(onRejected) } }; Copy CodeChain call then
_Believe that the Promise gasket above should be easy to understand, the chain call below is the difficulty and core of Promise; let's compare the promise/A+ specification step by step, let's first see how the specification is defined:
The then method must return a promise object
promise2 = promise1.then(onFulfilled, onRejected);
_In other words, each of the then methods returns a new Promise object so that our then method can be called continuously in a chain; therefore, the then method in the simple pad above is not applicable because it does not return anything and we simply override it to return a new Promise object whatever the then does:
Promise.prototype.then = function (onFulfilled, onRejected) { let promise2 = new Promise((resolve, reject)=>{ }) return promise2 } Copy Code
_We will continue to look at the execution of the n:
- If onFulfilled or onRejected returns a value of x, run the following Promise resolution: [[Resolve] (promise2, x)
- If onFulfilled or onRejected throws an exception e, promise2 must reject execution and return rejection e
- If onFulfilled is not a function and promise1 executes successfully, promise2 must execute successfully and return the same value
- If onRejected is not a function and promise1 refuses to execute, promise2 must refuse to execute and return the same reason
_First of all, we know that onFulfilled and onRejected will have a return value x after they are executed. Promise resolution is needed to process the return value x, let's go on; second, there's nothing to say about exception handling for onFulfilled and onRejected; third and fourth, it's really a problem, if onFulfilled and onRejectedIf two parameters are not passed, they continue to be downloaded (the transfer characteristics of values); for example:
var p = new Promise(function(resolve, reject){ setTimeout(function(){ resolve(3) }, 1000) }); p.then(1,1) .then('','') .then() .then(function(res){ //3 console.log(res) }) Copy Code
_Whatever value onFulfilled and onRejected pass in here, as long as it is not a function, it will continue to pass down until a function receives it; therefore, we improve the then method as follows:
//_this is an instance object of promise1 var _this = this onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason } var promise2 = new Promise((resolve, reject)=>{ if(_this.state === FULFILLED){ let x = onFulfilled(_this.value) resolvePromise(promise2, x, resolve, reject) } else if(_this.state === REJECTED){ let x = onRejected(_this.reason) resolvePromise(promise2, x ,resolve, reject) } else if(_this.state === PENDING){ _this.onFulfilled.push(()=>{ let x = onFulfilled(_this.value) resolvePromise(promise2, x, resolve, reject) }) _this.onRejected.push(()=>{ let x = onRejected(_this.reason) resolvePromise(promise2, x ,resolve, reject) }) } }) Copy Code
_We found that there is a resolvePromise in the function, which is the Promise resolution process mentioned above. It is the processing of the new promise2 and the previous execution result x. Because of the reusability, we pull it into a single function, which is also the first point defined in the specification above.
_Since the callback of the n is executed asynchronously, we need to put onFulfilled and onRejected executions into asynchronous execution and do some error handling at the same time:
//Other code omitted if(_this.state === FULFILLED){ setTimeout(()=>{ try { let x = onFulfilled(_this.value) resolvePromise(promise2, x, resolve, reject) } catch (error) { reject(error) } }) } else if(_this.state === REJECTED){ setTimeout(()=>{ try { let x = onRejected(_this.reason) resolvePromise(promise2, x ,resolve, reject) } catch (error) { reject(error) } }) } else if(_this.state === PENDING){ _this.onFulfilled.push(()=>{ setTimeout(()=>{ try { let x = onFulfilled(_this.value) resolvePromise(promise2, x, resolve, reject) } catch (error) { reject(error) } }) }) _this.onRejected.push(()=>{ setTimeout(()=>{ try { let x = onRejected(_this.reason) resolvePromise(promise2, x ,resolve, reject) } catch (error) { reject(error) } }) }) } Copy CodePromise Resolution Process
The Promise resolution process is an abstract operation that requires entering a promise and a value, expressed as [[Resolve] (promise, x). If X has the then method and looks like a Promise, the resolver attempts to make the promise accept the state of x; otherwise, it performs the promise with the value of X.
_This is an abstract sentence. Generally speaking, the process of resolving a promise requires passing in a new promise and a value X. If the incoming x is a thenable object (with the then method), the state of X is accepted:
//promise2: New Promise object //x: the return value of the last then //resolve:resolve for promise2 //reject:reject of promise2 function resolvePromise(promise2, x, resolve, reject) { } Copy Code_After defining the function, see the specific instructions:
- x equals promise
- If promise and x point to the same object, reject promise based on TypeError
- x is Promise
- If x is in the waiting state, promise needs to remain in the waiting state until x is executed or rejected
- If x is in the execution state, perform promise with the same value
- If x is in the rejection state, reject promise with the same reason
- x is an object or function
- Assign x.then to then
- If error E is thrown when taking the value of x.then, promise is rejected on the basis of e
- If the n is a function, call x as the scope of the function this.Pass in two callback functions as parameters, the first called resolvePromise and the second called rejectPromise:
- If resolvePromise is called with a value of y, run [[Resolve] (promise, y)
- If rejectPromise is called with r as the argument, promise is rejected with r as the argument
- If both resolvePromise and rejectPromise are called, or if they have been called multiple times by the same parameter, the first call takes precedence and the remaining calls are ignored
- If the n is not a function, perform promise with x as a parameter
- If x is not an object or function, perform promise with X as a parameter
First of all, if x and promise are equal, what is it like to go back to yourself?
var p = new Promise(function(resolve, reject){ resolve(3) }); //Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise> var p2 = p.then(function(){ return p2 }) Copy Code
_This will lead to a dead circle, so we must first eliminate this situation:
function resolvePromise(promise2, x, resolve, reject) { if (promise2 === x) { reject(new TypeError('Chaining cycle')); } } Copy Code
_Next, we will make a judgement on different situations. First, we will judge the situation where x is an object or a function:
function resolvePromise(promise2, x, resolve, reject) { if (promise2 === x) { reject(new TypeError('Chaining cycle')); } if (x !== null && (typeof x === 'object' || typeof x === 'function')) { //Function or Object } else { //Normal Value resolve(x) } } Copy Code
_If x is an object or a function, assigning x.then to the n is understandable, but what is the second point where taking the N might make a mistake?This is because you need to take into account all the errors (just in case a kid doesn't want a gentleman), and use it when someone implements the Promise object Object.defineProperty() Malicious errors that cause the program to crash, like this:
var Promise = {}; Object.defineProperty(Promise, 'then', { get: function(){ throw Error('error') } }) //Uncaught Error: error Promise.then Copy Code
_Therefore, we also need try/catch when we get the n:
//Other code omitted if (x !== null && (typeof x === 'object' || typeof x === 'function')) { //Function or Object try { let then = x.then } catch(e){ reject(e) } } Copy Code
_After taking out the then, go back to 3.3 and decide if it is a function, call x as the scope of the function, and pass in two callback functions as parameters.
//Other code omitted try { let then = x.then if(typeof then === 'function'){ then.call(x, (y)=>{ resolve(y) }, (r) =>{ reject(r) }) } else { resolve(x) } } catch(e){ reject(e) } Copy Code
_This way, our chain call can be called smoothly; however, there is a special case where if the y value of the resolve is still a Promise object, it should be continued, such as the following example:
var p1 = new Promise((resolve, reject)=>{ resolve('p1') }) p1.then((res)=>{ return new Promise((resolve, reject)=>{ resolve(new Promise((resolve, reject)=>{ resolve('p2') })) }) }) .then((res1)=>{ //Promise console.log(res1) }) Copy Code
_At this point the second one prints out a promise object; we should continue to recursively call resolvePromise (reference specification 3.3.1), so the complete code for the final resolvePromise is as follows:
function resolvePromise(promise2, x, resolve, reject){ if(promise2 === x){ reject(new TypeError('Chaining cycle')) } if(x && typeof x === 'object' || typeof x === 'function'){ let used; try { let then = x.then if(typeof then === 'function'){ then.call(x, (y)=>{ if (used) return; used = true resolvePromise(promise2, y, resolve, reject) }, (r) =>{ if (used) return; used = true reject(r) }) } else { if (used) return; used = true resolve(x) } } catch(e){ if (used) return; used = true reject(e) } } else { resolve(x) } } Copy Code
_Here, our Promise can also fully implement chain calls; then test the code with promises-aplus-tests and pass 872 tests perfectly.
Reference resourcespromise/A specification (English) promise/A+ specification (English) promise/A+ Specification (Chinese)
For more front-end information, please pay attention to the Public Number.
If you think it's good, pay attention to meNugget Home Page .More articles please visit Xie Xiaofei's Blog