preface
Promise I believe we are familiar with, and a handwritten Promise is also a hot interview, so let us step by step handwritten a Promise
Build the skeleton of Promise
According to the PROMISE /A+ specification, the Promsie constructor returns an instance of A Promise object that has A THEN method. In the THEN method, there are two parameters of function type, which are onfulfilled and onRejected.
Wherein, onfulfilled can obtain the value of the Promise object after resolve through the parameter, and onRejected can obtain the value of the Promise object after reject.
Let’s first write the constructors and then methods according to the specification
function Promise(executor){}Promise.prototype.then=function(onfulfilled,onrejected){}Copy the code
This is a pity. When the Promise constructor is called with new, the executor parameter resolve will be called at the end of the asynchronous operation. The value of resolve can be obtained from ondepressing, the first function parameter of the subsequent THEN method. When an error occurs, the executor argument reject is called and the error message is executed as a reject function argument. The error message can be found in the second argument onRejected to the subsequent THEN method, as shown below
function Promise(executor){
this.status = 'pending'
this.value = null
this.reason = null
The arrow function ensures that resolve and reject get the Promise instance value
const resolve=value= >{
this.value =value
}
const reject=reason= >{
this.reason = reason
}
executor(resolve,reject)
}
Promise.prototype.then = function(onfulfilled ,onrejected){
onfulfilled(this.value)
onrejected(this.error)
}
Copy the code
Improve the state of Promise
Let’s test the previous code
let promise = new Promise((resolve,reject) = >{
resolve('data')
reject('error')
})
promise.then(data= >{
console.log(data)
},error= >{
console.log(error)
})
Copy the code
Executing the code above prints data and error, whereas a normal promise would print data
Because the instance state of a Promise can only change from Pending to depressing or from Pending to Rejected. And once the state has changed, it is impossible to change again. The following code
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
// Add judgment to the resovle and Reject methods, which only allow the Promsie instance state to change from pending to depressing or from pending to Rejected
const resolve = value= > {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'}}const reject = reason= > {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
}
}
executor(resolve, reject)
}
Promise.prototype.then = function (onfulfilled , onrejected ) {
if (this.status === 'fulfilled'){
onfulfilled(this.value)
}
if (this.status === 'rejected'){
onrejected(this.reason)
}
}
Copy the code
Ok, our code executes fine, but Promise is designed to solve asynchronous problems, and our code executes synchronously, so don’t worry, let’s fix the most important asynchronous logic
Write asynchronous logic for promises
Asynchronous preliminary implementation
Let’s test the following code first
let promise = new Promise((resolve,reject) = >{
setTimeout(() = >{
resolve('data')},2000)
})
promise.then(data= >{
console.log(data)
})
Copy the code
I wait for 2s, and nothing comes out, right? ^? Normally, data will be output after two seconds
The reason is that the ondepressing in our then method is performed synchronously, and this. Status is still pending when it is performed, and the ondepressing is not performed 2s later.
The ondepressing method should be called at the appropriate time, which is when the developer calls Resolve. So we can save the ondepressing method passed in by the developer when the state is pending, and then execute it in the Resolve method. The following code
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
// Set the default value to function elements
this.onfulfilledFunc = Function.prototype
this.onrejectedFunc = Function.prototype
const resolve = value= > {
// Determine if value is an instance of Promise, if then is applied to it first
if(value instanceof Promise) {return value.then(resolve,reject)
}
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onfulfilledFunc(this.value)
}
}
const reject = reason= > {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onrejectedFunc(this.reason)
}
}
executor(resolve, reject)
}
Promise.prototype.then = function (onfulfilled , onrejected ) {
if (this.status === 'fulfilled'){
onfulfilled(this.value)
}
if (this.status === 'rejected'){
onrejected(this.reason)
}
if (this.status === 'pending') {this.onfulfilledFunc = onfulfilled
this.onrejectedFunc = onrejected
}
}
Copy the code
Put into a task queue
Let’s look at this test
let promise = new Promise((resolve,reject) = >
{
setTimeout(() = >{
resolve('data')},2000)
})
promise.then(data= >{
console.log(data)
})
console.log(1)
Copy the code
Our code prints data, and then prints 1.
Normally, it prints 1 and then data.
The reason is that console.log(1) is a synchronous task, and asynchronous task queues are executed only when the stack of synchronous tasks is empty. Therefore, put resolve and Reject on the task queue.
We simply put a setTimeout into it to ensure asynchronous execution (in fact, promises are microtasks and can be simulated using MutationObserver)
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onfulfilledFunc = Function.prototype
this.onrejectedFunc = Function.prototype
const resolve = value= > {
if(value instanceof Promise) {return value.then(resolve,reject)
}
setTimeout(() = >{
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onfulfilledFunc(this.value)
}
})
}
const reject = reason= > {
setTimeout(() = >{
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onrejectedFunc(this.reason)
}
})
}
executor(resolve, reject)
}
Promise.prototype.then = function (onfulfilled , onrejected ) {
if (this.status === 'fulfilled'){
onfulfilled(this.value)
}
if (this.status === 'rejected'){
onrejected(this.reason)
}
if (this.status === 'pending') {this.onfulfilledFunc = onfulfilled
this.onrejectedFunc = onrejected
}
}
Copy the code
Save the callback function
Look at the following test
/ / test
let promise = new Promise((resolve,reject) = >
{
setTimeout(() = >{
resolve('data')},2000)
})
promise.then(data= >{
console.log(` 1:${data}`)
})
promise.then(data= >{
console.log(` 2:${data}`)})Copy the code
In theory it should output
1:data
2:data
Copy the code
But only 2:data is output because the onledFunc in the second THEN method overwrites the onledFunc in the first THEN method
So we just need to store the onimplemented func of onFulfilldedArray’s then method in an array, and just implement the methods of onFulfilldedArray’s array as the Promise is decided. The same goes for onRejected
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onfulfilledArray = []
this.onrejectedArray = []
const resolve = value= > {
if(value instanceof Promise) {return value.then(resolve,reject)
}
setTimeout(() = >{
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
// The forEach method executes the methods in turn
this.onfulfilledArray.forEach(func= >{
func(value)
})
}
})
}
const reject = reason= > {
setTimeout(() = >{
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onrejectedArray.forEach(func= >{
func(reason)
})
}
})
}
// If an error occurs in the constructor, the Promsie instance state will automatically change to Rejected
try{
executor(resolve, reject)
}catch(e){
reject(e)
}
}
Promise.prototype.then = function (onfulfilled , onrejected ) {
if (this.status === 'fulfilled'){
onfulfilled(this.value)
}
if (this.status === 'rejected'){
onrejected(this.reason)
}
if (this.status === 'pending') {// Store the ondepressing in an array
this.onfulfilledArray.push(onfulfilled)
this.onrejectedFunc.push(onrejected)
}
}
Copy the code
Next we continue to implement the chain call effect of the THEN method
A chain call to promsie then
Preliminary implementation of chain call
Let’s first look at what happens if the first THEN method returns a non-Promise instance
let promise = new Promise((resolve,reject) = >
{
setTimeout(() = >{
resolve('data')},3000)
})
promise.then(data= >{
console.log(`first ${data}`)
return `second ${data} `
})
.then(data= >{
console.log(data)
})
Copy the code
Each THEN method should return a Promise instance
Promise.prototype.then = function (onfulfilled , onrejected ) {
//Promise penetrates the implementation by giving a default value to the function that returns its argument if a non-function value is passed to the then() function
onfulfilled = typeof onfulfilled === 'function'? onfulfilled:data= >data
onrejected = typeof onrejected === 'function' ? onrejected:error= >{throw error};
// promise2 will be the return value of the THEN method
let promise2
if (this.status === 'fulfilled') {return promise2 = new Promise((resolve,reject) = >{
setTimeout(() = >{
try{
// The resolve value of this new promise2 is the result of ondepressing
let x = onfulfilled(this.value)
resolve(x)
}catch(e){
reject(e)
}
})
})
}
if (this.status === 'rejected') {return promise2 = new Promise((resolve,reject) = >{
setTimeout(() = >{
try {
// The resolve value of this new promise2 is the result of onRejected
let x = onrejected(this.value)
resolve(x)
} catch (e) {
reject(e)
}
})
})
}
// Promise2 is resolved when the ononledarray or onRejectedArray array functions are executed successively after asynchronous processing ends
// A function of the ononledarray or onRejectedArray array switches the state of promise2 and performs the resolution
if (this.status === 'pending') {return promise2 =new Promise((resolve,reject) = >{
this.onfulfilledArray.push(() = >{
try{
let x = onfulfilled(this.value)
resolve(x)
}catch(e){
reject(e)
}
})
this.onrejectedArray.push(() = >{
try{
let x = onrejected(this.reason)
resolve(x)
}
catch(e){
reject(e)
}
})
})
}
}
Copy the code
Code execution successful
Perfect chain calls
Let’s look at this example
const promise = new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve('data')},3000)
})
promise.then(data= > {
console.log(data)
return new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve(`${data} next`)},1000)
})
})
.then(data= > {
console.log(data)
})
Copy the code
According to the Promise /A+ specification, X can be either A common value or A promise instance, so we abstract the resolvePromise method for unified processing, and the code is as follows
Promise.prototype.then = function (onfulfilled , onrejected ) {
onfulfilled = typeof onfulfilled === 'function'? onfulfilled:data= >data
onrejected = typeof onrejected === 'function' ? onrejected:error= >{throw error};
let promise2
if (this.status === 'fulfilled') {return promise2 = new Promise((resolve,reject) = >{
setTimeout(() = >{
try{
let x = onfulfilled(this.value)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
})
}
if (this.status === 'rejected') {return promise2 = new Promise((resolve,reject) = >{
setTimeout(() = >{
try {
let x = onrejected(this.value)
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
})
})
}
if (this.status === 'pending') {return promise2 =new Promise((resolve,reject) = >{
this.onfulfilledArray.push(() = >{
try{
let x = onfulfilled(this.value)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
this.onrejectedArray.push(() = >{
try{
let x = onrejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
}
catch(e){
reject(e)
}
})
})
}
}
Copy the code
Finally, implement the resolvePromise function
const resolvePromise = (promise2, x, resolve, reject) = > {
// Execute reject when X and promise2 are equal, i.e., when ondepressing returns promise2
if (x === promise2) {
reject(new TypeError('error due to circular reference'))}// Whether you have performed onfulfilled or onRejected
let consumed = false
let thenable
// If Promsie is returned
if (x instanceof Promise) {
if (x.status === 'pending') {
x.then(function (data) {
// recursive call
resolvePromise(promise2, data, resolve, reject)}, reject
)
} else {
x.then(resolve, reject)
}
return
}
let isComplexResult = targert= > (typeof targert === 'function' || typeof targert === 'object') && (targert ! = =null)
// If the Promsie type is returned
if (isComplexResult(x)) {
try {
thenable = x.then
// Determine if the return value is of type PROMISE
if (typeof thenable === 'function') {
thenable.call(x, function (data) {
if (consumed) {
return
}
consumed = true
// recursive call
return resolvePromise(promise2, data, resolve, reject)
}, function (error) {
if (consumed) {
return
}
consumed = true
return reject(error)
})
}
else {
resolve(x)
}
} catch (e) {
if (consumed) {
return
}
consumed = true
return reject(e)
}
}
// If the value is normal
else {
resolve(x)
}
}
Copy the code
Now that we’ve nailed down the chain calls to THEN, which are at the heart of handwritten promises, we’ll implement catch methods and other static methods together in the next article