preface
This article is mainly based on promise-polyfill source code analysis of the implementation principle of promise, hope that through a popular way to sort out the implementation of promise clear, the article content is long, I hope you have the honor to see this article can calm down to carefully analyze, if the code is wrong, please correct. Promise-polyfill address: github.com/taylorhakes…
Step 1 basic edition
First, specify the status code:
- 0 = ‘PENDING’
- 1 = ‘FULFILLED’
- 2 = ‘REJECTED’
- 3 = Promise state 3 indicates that resolve(PROMISE) is passed as a promise object, which will be discussed later
// Why resolve and reject are outside of a CopyPromise
//1. There is no resolve method on the native Promise instance, in order to be consistent with promis
//2. Native Pormise has a static property promise.resolve, in order not to generate any ambiguity
function resolve(self, newValue) {
// Change the status to successful
self.status = 1
self._value = newValue
self._deferreds.forEach(deferred= > {
deferred.onFulfilled(newValue)
})
}
/ / reject again
function reject(self, newValue) {
// Change to failed state
self.status = 2
self._value = newValue
self._deferreds.forEach(deferred= > {
deferred.onRejected(newValue)
})
}
class CopyPromise {
constructor(fn) {
this._value = null
// A queue for then callbacks, including successful and failed callbacks
this._deferreds = []
// The initial state is PENDING. After resolve is executed, change the state to 1= depressing
this.status = 0
// fn is passed when new
Pass resolve and reject back to fn
Resolve and reject are wrapped in function to pass this to the resolve method
// Put it in the try tatch to catch the fn exception error
try{
fn(
(newValue) = > resolve(this, newValue),
(newValue) => reject(this, newValue)
)
}catch(err){
reject(this, err)
}
}
then = (onFulfilled, onRejected) = > {
// This is a big pity, onFulfilled callback
// Wait until the external resolve is executed to traverse the queue and execute
this._deferreds.push({
onFulfilled,
onRejected
})
}
}
Copy the code
To test this, OK executes the following code, which appears to be fine
const p = new CopyPromise(resolve= >{
setTimeout((a)= >{
resolve(123)},500)
})
p.then(res= >{
console.log(res,'res')})Copy the code
But there’s a problem, so let’s change the code and see what happens
const p = new CopyPromise(resolve= >{
// Delete the setTimeout
resolve(123)
})
p.then(res= >{
console.log(res,'res')})Copy the code
The result of the above code is that the then callback is not executed. What happens when you take setTimeout out and the code doesn’t execute? Think about why?
Let’s look at the execution of the code
-
The new CopyPromise callback is executed synchronously, so resolve is executed at new, yes but then is not executed, We know that then mainly collects callbacks and waits for resolve to be called before executing the collected callback methods. Resolve will execute first and resolve will not execute after then collects callbacks, so the callback will not execute.
-
This is also the onFulfilled Promise specification. OnFulfilled should be performed after the event loop that calls THE THEN, and a new stack will be performed asynchronously. This problem can be solved by relying on event loop handling, which is commonly known as putting resolve in an event loop (such as setTimeout) to give then a chance to execute first to collect the callback.
-
The Promise specification states: OnFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1]. Promisesaplus.com/ : OnFulled and onRejected are executed after an event loop that calls THEN, via macro tasks (e.g. SetTimeout) or microtasks.
Modify the code
function resolve(self, newValue) {
// Add a setTimeout
setTimeout((a)= > {
self._value = newValue
self._deferreds.forEach(deferred= > {
deferred.onFulfilled(newValue)
})
})
}
function reject(self, newValue) {
// Add a setTimeout
setTimeout((a)= > {
self._value = newValue
self._deferreds.forEach(deferred= > {
deferred.onRejected(newValue)
})
})
}
Copy the code
ok! The code should then execute normally.
2. The second then chain call
So let’s think about how to implement this call promise.then(), which is easy to think of as long as promsie has a then method on it so promise.then().then(), The then() method returns a promise instance after promise.then() is executed. Ok, change the code so that THEN returns a Promise instance.
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
// Make then return a Promise instance for the chain-call
then = (onFulfilled, onRejected) = > {
const p = new CopyPromise((a)= >{})this._deferreds.push({
onFulfilled,
onRejected
})
return p
}
}
Copy the code
Test the chain call
const p = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(123)},1000)
})
p.then((res) = > {
console.log(res)
return res
}).then(res= > {
console.log(res)
})
Copy the code
We found that the callback for the second THEN was not executed. Why? What about the chain call?
The first THEN callback is ok, because we did resolve in the new CopyPromise callback, but resolve was not called before the second THEN callback, so the then callback has no chance to execute. So let’s think about how do we solve this problem here?
If a chained call requires us to call resolve manually inside the Promise, where should we call resolve?
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
then = (onFulfilled, onRejected) = > {
const p = new CopyPromise((a)= >{})// First, you need to add a callback to the queue only when the state is 0
if (this.status === 0) {
// The new Promise instance is also stored here, ready for the chain call
this._deferreds.push({
onFulfilled: onFulfilled || null.onRejected: onRejected || null.promise: p // This is a new one})}return p
}
}
// The answer is to call resolve in the resolve method
function resolve(self, newValue) {
setTimeout((a)= > {
self._value = newValue
self._deferreds.forEach(deferred= > {
let res = deferred.onFulfilled(newValue)
// In the then method, we have added a new promise object to the _Deferreds queue. This is the object that then returns
// Passing the promise instance directly to resolve is equivalent to manually invoking the resolve method during the chained call, and the second THEN callback will be executed when resolve executes
// This forms a recursive call, which is the key to implementing chained calls
// The deferred. Promise: then the promise instance of new
// then returns the value passed in the callback method
// Consider that calling resolve is equivalent to executing a method from the next then collection queue (deferred.promise._deferreds).
resolve(deferred.promise, res)
})
})
}
Copy the code
Let’s test the chain call
const p = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(123)},1000)
})
p.then((res) = > {
console.log(res, '1')
return res
}).then(res= > {
console.log(res, '2')})Copy the code
We already know how this chain call works, but if we look at the source code for the promise-polyfill method, it’s a little different from the chain call we’ve implemented. Promise-polyfill puts the logic into the Handle method, So since we’re going to use promise-polyfill to resolve promises, let’s change the code.
// Separate the handle method
/ / parameters shows that the self is the promise, deferred is the one in the queue {onFulfilled onRejected, promise}
// Our previous chained call only dealt with the success state, so let's add the failure logic as well
function handle(self, deferred) {
If the state is 0, the callback will be added to the queue. If the state is 0, the callback will be executed successfully or failed
if (self.status === 0) {
self._deferreds.push(deferred)
return
}
// Perform different callbacks based on the current state
let cb = self.status === 1 ? deferred.onFulfilled : deferred.onRejected
// Note that cb may be null, for example: p.ten.then (), which is null if there is no callback in then. Null is the value assigned to the THEN method in CopyPrimise source code
// If there is no callback then we call resolve or reject to execute then chain by chain
if(cb === null){
(self.status === 1 ? resolve : reject)(deferred.promise, self._value)
// Return is required, because the code is executed further down when a callback is passed in the then
return
}
// If a callback is passed in the then
let res = cb(self._value)
// Execute resolve again to ensure the chained call
resolve(deferred.promise, res)
}
/ / CopyPromise
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
then = (onFulfilled, onRejected) = > {
const p = new CopyPromise((a)= >{})// Replace the original logic with handle
handle(this, {onFulfilled: onFulfilled || null.onRejected: onRejected || null.promise: p
})
return p
}
}
// Modify the resolve method
// Since all the processing logic is in handle, we should call handle in resolve
function resolve(self, newValue) {
setTimeout((a)= > {
self.status = 1
self._value = newValue
self._deferreds.forEach(deferred= > {
Ondepressing is a big pity. This is a big pity. // Before, only deferred. Ondepressing is also a success callback
// let res = deferred.onFulfilled(newValue)
// resolve(deferred.promise, res)
handle(self, deferred)
})
// Clear the queue
self._deferreds = []
})
}
// reject method
function reject(self, newValue) {
setTimeout((a)= > {
self.status = 2
self._value = newValue
self._deferreds.forEach(deferred= > {
handle(self, deferred)
})
// Clear the queue
self._deferreds = []
}, 0)}Copy the code
Let’s test the chain call again
const p = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(123)},1000)
})
p.then((res) = > {
console.log(res, '1')
return res
}).then(res= > {
console.log(res, '2')})Copy the code
There is no problem with chain calls, but there is one problem that is not solved, see the following code
const p = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
// We make a new promise in resolve and pass it to resolve
resolve(new CopyPromise((resolve,reject) = >{
resolve(123)}})),1000)})// Instead of printing 123, we print an instance of a promise, meaning that the _value on the instance becomes a Promise object
p.then((res) = > {
console.log(res, '1')
return res
}).then(res= > {
console.log(res, '2')})Copy the code
So what do we do when we pass a Promise object to Resolve? First we add status=3 to resolve to pass a Promise instance
function resolve(self, newValue) {
setTimeout((a)= > {
// Add a judgment, change the state to 3 if the value passed is a Promise instance
if (newValue instanceof CopyPromise) {
self.status = 3
} else {
self.status = 1
}
self._value = newValue
self._deferreds.forEach(deferred= > {
handle(self, deferred)
})
self._deferreds = []
})
}
function handle(self, deferred) {
// If the state is 3, we change the current self promise instance to the promise instance passed in resolve
// Then pass the final resolve to get the true value 123
// The reason for using the while loop is that multiple layers of promises can be nested in resolve. For example, resolve has two layers in the comment below
/** * new CopyPromise((resolve, reject) => { setTimeout(() => { resolve(new CopyPromise((resolve, reject) => { resolve(new CopyPromise((resolve, reject) => { resolve(123) })) })) }, 1000) }) */
// So use while until resolve(123), which is the value we need
while (self.status === 3) {
self = self._value;
}
if (self.status === 0) {
self._deferreds.push(deferred)
return
}
let cb = self.status === 1 ? deferred.onFulfilled : deferred.onRejected
if(cb === null){
(self.status === 1 ? resolve : reject)(deferred.promise, self._value)
return
}
let res = cb(self._value)
resolve(deferred.promise, res)
}
Copy the code
Ok, to the principle of the promise of chain invocation is combed, some places need to think, if you see here is there can doubt about chain calls back to look at a few times, some key points need to consider the why, or you can go to consult some other information, the expression of each person is different, Or you have more logical thinking of the article, it is recommended to master the then chain call before continuing to read.
3. The third step is to improve other methods of promise
Catch method
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
then = (onFulfilled, onRejected) = > {
const p = new CopyPromise((a)= > { })
handle(this, {onFulfilled: onFulfilled || null.onRejected: onRejected || null.promise: p
})
return p
}
// The catch method is equivalent to calling reject
// Then calls the first successful callback (onRejected) and the second callback (onRejected)
catch = (onRejected) = > {
return this.then(null, onRejected)
}
}
Copy the code
Test catch
const p = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
reject('Wrong')},1000)
})
p.then((res) = > {
console.log(res)
return res
}).catch(err= > {
// Print an error
console.log(err)
})
Copy the code
Promise. All methods
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
/ /... Omit other code
static all = (arr) = > {
let args = Array.prototype.slice.call(arr);
// First the outermost layer returns a Promise instance for the chain-call
return new CopyPromise((resolve, reject) = > {
// First we need to loop through the array, which is executed concurrently
for (let i = 0; i < arr.length; i++) {
res(i, arr[i])
}
Resolve can only be executed if all promises have been completed
let remaining = arr.length
// The way to handle each promise
function res(index, val) {
// Use this judgment if an instance of a promise is passed
if (val && typeof val === 'object') {
let then = val.then
if (typeof then === 'function') {
// Why call then here?
// Because the passed promise may contain nested promises, this is the same principle as above when we wrote the chain call where the state was 3
// const promise1 = new CopyPromise(resolve => {
// resolve(new CopyPromise(resolve => {
// resolve('promse1')
/ /}))
// })
// Then you need to continue implementing the nested promise and wait for it to end, which is the recursive effect
then.call(
val,
function (val) {
res(index, val)
},
reject
)
// Return is required because val is a promise object and not the desired value, so the code can't continue
return}}Resolve () returns a value that is no longer a promise object
// Place val at the index position
args[index] = val
// Remaining =0 indicates that all promises are fulfilled
if (--remaining === 0) { resolve(args); }})}}Copy the code
Test promise.all
const p1 = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(1)},200)})const p2 = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(2)},300)})const p3 = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(3)},400)})// Result [1, 2, 3]
CopyPromise.all([p1,p2,p3]).then(res= >{
console.log(res)
})
Copy the code
Resolve and Promie. Reject methods
These are relatively simple methods that call resolve, reject
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
/ /... Omit other code
static resolve = (value) = > {
return new CopyPromise(function (resolve, reject) {
resolve(value)
})
}
static reject = (value) = > {
return new CopyPromise(function (resolve, reject) {
reject(value)
})
}
}
Copy the code
Promise. Race method
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
/ /... Omit other code
/ / competition
// Return the result as soon as possible
static race = (arr) = > {
// This is the outer promise for the convenience of chain calls
return new CopyPromise(resolve= > {
for (var i = 0, len = arr.length; i < len; i++) {
// Call copyPromise.resolve, or you can manually use new CopyPromise
// The key is to make sure that the first promise is fulfilled, and that the subsequent promise is not fulfilled
For example, [promsie1, promise2, promise3], there are three items in the array
// Then passes the resolve of the outer promise.
// When resolve is complete, the status of the outer instance is changed to 1= success (or 2= failure), and the outer promise _deferreds is cleared
// Note that the external _deferreds is when we actually call race, for example copyPromise.race ([P1,p2]). Then ((res)=>{}), where then collects callbacks to the _deferreds queue.
[promsie1, promise2, promise3], promsie1, promise3]
// The state of the outer promise instance is successful, and the _deferreds have been emptied, so the resolve forEach loop will no longer run, and the Handle will no longer runCopyPromise.resolve(arr[i]).then(resolve); }}}})Copy the code
Test out Prmise.race
const p1 = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(1)},200)})const p2 = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(2)},300)})const p3 = new CopyPromise((resolve, reject) = > {
setTimeout((a)= > {
resolve(3)},100)
})
CopyPromise.race([p1, p2, p3]).then(res= > {
// Print out 3 correctly
console.log(res)
})
Copy the code
Promise. Finally method
class CopyPromise {
constructor(fn) {
this._value = null
this._deferreds = []
this.status = 0
fn((newValue) = > resolve(this, newValue), (newValue) => reject(this, newValue))
}
/ /... Omit other code
// First, finally will be executed regardless of the state, and finally's callback has no arguments
// Call the then method in finally
// The value in the this.then callback is the current value. Value is not passed to fn(), finally callback has no arguments
// cp.resolve (fn()) is a finally callback, and then the then is a callback that returns the vulue
// For example, then is finally called so that the value can be retrieved from then
// p1.then(res=>{
// console.log(res)
// }).finally(()=>{
// console.log('finally')
// }).then(res=>{
// console.log(res)
// })
Return CP. Resolve (fn())) return CP. Resolve (fn()
// Here we are consistent with promise-polyfill
finally = (callback) = > {
// Use constructor. Resolve is a static method that cannot be retrieved from this.resolve
// It is usually the constructor name. Method name
const constructor = this.constructor
return this.then(
(value) => {
return constructor.resolve(callback()).then(() => value)
}, (value) => {
return constructor.resolve(callback()).then(() => constructor.reject(value))
}
)
}
}
Copy the code
Test promise.Finally
const p1 = new CopyPromise(function (resolve, reject) {
setTimeout(resolve, 500.123);
});
p1.then(res= > {
console.log(res)
return res
})
p1.then(res= > {
console.log(res)
return res
}).finally((a)= > {
console.log('finally')})Copy the code
This promise the realization of the basic principle has been finished, and of course many points can be optimized, other details you can refer to https://github.com/taylorhakes/promise-polyfill/blob/master/src/index.js, This article is mainly based on promise-Polyfill source code to make the promise implementation principle clear, hoping to help you better understand the promise.
References:
- promise-polyfill
- Promise specification