A, bear
This article is the second in a series of articles titled “Make a Promise With You Come True.” So, if you haven’t read the first one yet, you might want to check out the package and make a Promise come true for you.
In the first article we have implemented A MyPromise that is very similar in form to A Promise, but although the form is somewhat similar, it is far from A Promise that fully conforms to the Promise A+ specification. Now MyPromise:
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = []
this.rejectedCallbacks = []
let resolve = (value) = > {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
this.resolvedCallbacks.forEach(fn= > fn(this.data))
}
}
let reject = (reason) = > {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
this.rejectedCallbacks.forEach(fn= > fn(this.reason))
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function(onResolved, onRejected) {
let promise2
if (this.status === 'pending') {
this.resolvedCallbacks.push(onResolved)
this.rejectedCallbacks.push(onRejected)
}
if (this.status === 'fulfilled') {
onResolved(this.data)
}
if (this.status === 'rejected') {
onRejected(this.reason)
}
}
Copy the code
Now the problem with this MyPromise is that it can’t make a chain call. When we use promises, we have code like this:
let promise1 = new Promise(function(resolve, reject) {
// Simulate asynchrony
setTimeout((a)= > {
let flag = Math.random() > 0.5 ? true : false
if (flag) {
resolve('success')}else {
reject('fail')}},1000)
})
promise1.then(res= > {
return 1
}, err => {
return err
}).then(res= > {
console.log(res)
})
Copy the code
But for now, MyPromise’s THEN is a one-off, executed and gone, with nothing returned that can then.
Second, chain call
According to the Promise A+ specification, A Promise instance must return A new Promise instance after then, so that then can be used to implement chained calls.
Before we implement the THEN method, let’s talk briefly about the technique of implementing chain calls. In general, there are two ways to implement a chain call:
- When a method executes, it returns itself, that is, this, as jQuery does
- The other is the Promise method, which returns a new Promise instance when the THEN method executes
1, jQuery chain call idea
function $(selector) {
return new jQuery(selector)
}
class jQuery {
constructor(selector) {
let slice = Array.prototype.slice
this.domArray = slice.call(document.querySelectorAll(selector))
// ...
}
append() {
// ...
return this
}
addClass() {
/ /..
return this}}Copy the code
Construct a jQuery object whose methods return this, the same jQuery object that was constructed in the first place. $(‘.app’).append($span).addClass(‘test’)
2, the idea of chain call promise
class Promise {
constructor() {
/ /...
}
then() {
/ /...
let promise2 = new Promise(a)// That's the point
return promise2
}
}
/ / use
let promise1 = new Promise(a)let promise2 = promise1.then()
let promise3 = promise2.then()
// It can also be written like this
promise1.then().then()
Copy the code
This way, each time the THEN method executes, it returns a completely new Promise instance and you can move on to the then.
Function of then method
At this point, we finally get to the heart of the Promise, the then method. Throughout the Promise A+ specification, most of it is about how THEN is implemented, and there is nothing wrong with saying that then methods are at the heart of promises.
Let’s first define what the then method does:
- The then method must return a new Promise instance in order to do the chain callback, as described above
- The state of the new Promise instance returned by the THEN method depends on the state of the current instance and the state of the value returned by the callback
- The callback value returned by the current Promise state can be retrieved by the new instance
Let’s break it down.
First, the method of implementing the chain call, described above, will not be repeated.
Second, the new Promise instance state returned by the THEN method depends on the current instance state and the state of the callback return value. To understand this, take a closer look at the following example:
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > {
resolve('success')},1000)})let promise2 = promise1.then(res= > {
return res
})
console.log(promise1) / / pendding state
console.log(promise2) / / pending state
setTimeout((a)= > {
console.log(promise1) // Success status
console.log(promise2) // Success status
}, 1000)
Copy the code
As you can see from the code above, if promise1 is pending, then promisE2 must also be pending. The status of promise1 can be determined only when the status of promisE1 is determined. So when the promise1 state is fixed, is the promisE2 state fixed? No, look at the return values of the two functions registered in the THEN of promise1. Look at the following example:
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > {
resolve('success')},1000)})let promise2 = promise1.then(res= > {
return Promise.reject('refuse') // Note that this callback returns a reject state Promise
})
console.log(promise1) / / pending state
console.log(promise2) / / pending state
setTimeout((a)= > {
console.log(promise1) // Success status
console.log(promise2) // This is a failed state
}, 1000)
Copy the code
From the two examples above, we can conclude that when constructing a new Promise in the THEN method, we should not only use different strategies based on the state of the current Promise instance, but also consider the results of the two callbacks passed by the current THEN method.
The third point is that when the result of a callback passed in the then method of promise1 is available to the next instance, it is easy to get the result of the callback and pass it in either resolve or Reject.
If you don’t understand this, we’ll talk about it when we implement the THEN method.
Fourth, the concrete implementation of then method
The THEN method needs to return a new Promise instance, and the new instance needs to be constructed based on the state of the current instance. So the code for MyPromise’s then method looks like this:
MyPromise.prototype.then = function(onResolved, onRejected) {
let promise2
if (this.status === 'pending') {
promise2 = new MyPromise((resolve, reject) = >{})// this.resolvedCallbacks.push(onResolved)
// this.rejectedCallbacks.push(onRejected)
}
if (this.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) = >{})// onResolved(this.data)
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) = >{})// onRejected(this.reason)
}
return promise2
}
Copy the code
Declare a promisE2 to return and assign it a value based on the current state of the current instance. Here, we comment out the original code in each if judgment. The next step is to construct promise2 and see how its executor function is implemented. Let’s start with a discussion.
1. When the current instance status is Pending
According to the previous discussion, when the current is pending, the state of promise2 cannot be determined until it is determined. So this part of the code in MyPromise looks like this
MyPromise.prototype.then = function(onResolved, onRejected) {
let promise2
if (this.status === 'pending') {
promise2 = new MyPromise((resolve, reject) = > {
// Declare a success function
function successFn(value) {
let x = onResolved(value)
}
// Declare a failed function
function failFn(reason) {
let x = onRejected(reason)
}
// Push the success function to the resolvedCallbacks of the current instance
this.resolvedCallbacks.push(successFn)
// Push the failed function to rejectedCallbacks of the current instance
this.rejectedCallbacks.push(failFn)
})
}
if (this.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) = >{})// onResolved(this.data)
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) = >{})// onRejected(this.reason)
}
return promise2
}
Copy the code
If the current Promise’s then call is pending, we declare successFn and failFn and push them to resolvedCallbacks and rejectedCallbacks, respectively. This leaves the timing of successFn and failFn execution up to the current Promise instance. When the state of the current Promise instance is determined, successFn or failFn is executed, and you can get the callback result by calling onResovled or onRejected.
This step is critical because if the current Promise instance is in a pending state, the then method that returns a new promise2 must wait for its status to be determined before receiving its success or failure callback. The state of promisE2 is then determined based on the result X after the callback is executed.
The sequence of the whole process is as follows:
- The current instance handles asynchronous code and is in a pending state
- The then method executes, and the current instance state is still Pending
if(this.status === 'pending')
In the branch - Construct and return a promise2 whose state is pending
- The promise2 executor declares successFn and failFn when pushing the resolvedCallbacks and rejectedCallbacks of the current instance
- Resolve or Reject is called to the current instance, and successFn or failFn stored in resolvedCallbacks is executed
- Then onResovled or onRejected is executed, resolve or Reject is returned, and x is returned
- Processing for x…
2. The current instance status isfulfilled
orrejected
This is a simple call to onResovled or onRejected, which is the first and second parameters passed by the current instance then.
MyPromise.prototype.then = function(onResolved, onRejected) {
let promise2
if (this.status === 'pending') {
promise2 = new MyPromise((resolve, reject) = > {
function successFn(value) {
let x = onResolved(value)
}
function failFn(reason) {
let x = onRejected(reason)
}
this.resolvedCallbacks.push(successFn)
this.rejectedCallbacks.push(failFn)
})
}
if (this.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) = > {
// The current instance's resolve or reject has been executed
// this.data or this.reason
let x = onResolved(this.data)
})
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) = > {
// The current instance resolve or reject has been executed
let x = onRejected(this.reason)
})
}
return promise2
}
Copy the code
By this point, the current instance’s resolve or reject is invoked, the status is established, and this.data or this.reason has a value, which can be obtained by onResoved or onRjected by then.
So our then here, promise2 is already getting the callback result of the current instance. Let’s see where it is in practice. Look at the following example:
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > {
resolve('success')},1000)})function onResolved(res) {
// Do the processing here
return xxx // the XXX returned here is actually the x in the code above
}
function onRejected(err) {
/ / processing
return xxx // the XXX returned here is the x in the code above
}
let promise2 = promise1.then(onResolved, onRjected)
Copy the code
When the THEN method is executed, we have successfully obtained the callback value x of the current instance, and we will unify this value and determine the state of promise2 based on the resolve or Reject method when X calls the construction of promise2.
3. Handle X
The specification has a section on how to handle X to determine the state of promise2. This is referred to in Section 2.3 of The specification as The Promise Resolution Procedure, which performs targeted processing based on The possible values of x. X can be the following values:
- X is the instance of promise2 itself, as explained later
- X is our MyPromise instance that we wrote ourselves
- When x is a function or an object
- X is not a function or an object
We need to write a function resolve_promise to handle x and determine the state of promise2
MyPromise.prototype.then = function(onResolved, onRejected) {
let promise2
if (this.status === 'pending') {
promise2 = new MyPromise((resolve, reject) = > {
function successFn(value) {
let x = onResolved(value)
// Notice here
resolve_promise(promise2, x, resolve, reject)
}
function failFn(reason) {
let x = onRejected(reason)
// Here too
resolve_promise(promise2, x, resolve, reject)
}
this.resolvedCallbacks.push(successFn)
this.rejectedCallbacks.push(failFn)
})
}
if (this.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) = > {
let x = onResolved(this.data)
// Here too
resolve_promise(promise2, x, resolve, reject)
})
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) = > {
// And here
let x = onRejected(this.reason)
resolve_promise(promise2, x, resolve, reject)
})
}
return promise2
}
// resolve_promise
function resolve_promise(promise2, x, resolve, reject) {}Copy the code
The resolve_promise accepts 4 parameters: the currently constructed instance of promise2 and the result x obtained through the current callback. Resolve and reject are two methods used to determine the state of the promise2. Because the state of promise2 is only known if resolve or reject is called.
Next, we deal with each of the four possible states of X, which are specified in the specification:
3.1 If X is the promise2 instance itself
This is only possible if the current instance is pending, as shown in the following example:
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > {
resolve('success')},1000)})function onResolved(res) {
return promise2
}
let promise2 = promise1.then(onResolved)
promise2.then(res= > {
console.log(res)
}, err => {
console.log(err)
// TypeError: Chaining cycle detected for promise is printed here
})
Copy the code
So when this happens, the specification simply rejects promise2 with reject and passes a TypeError
function resolve_promise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(new TypeError('Chaining cycle detected for promise'))
return // return does not need to go down}}Copy the code
3.2 If x is an instance of MyPromise that we wrote ourselves
The usage scenario is this:
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > {
resolve('success')},1000)})function onResolved(res) {
// The promise returned here is the x we are dealing with
return new Promise(function(resolve, reject) {
reject('fail')})}let promise2 = promise1.then(onResolved)
Copy the code
In this case, the state and value of x are used as the state and value of promise2 in three different scenarios:
- If x is pending, our promise2 must wait until x succeeds or fails
- This is a big pity. If X is a big pity, then the promise2 state will be marked as a success state, and the transferred value will also be the success value in X
- Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2 = Promise2
The resolve_PROMISE method changes as follows:
function resolve_promise(promise2, x, resolve, reject) {
// x and promise2 reference the same situation
if (x === promise2) {
reject(new TypeError('Chaining cycle detected for promise'))
return
}
If x is an instance of MyPromise
if (x instanceof MyPromise) {
x.then(function (v) {
resolve_promise(promise2, v, resolve, reject)
}, function (t) {
reject(t)
}
return}}Copy the code
You may not understand this, but there are three cases, right? This code doesn’t do any of those things either. There are three cases where I can get the resolve or reject value from the X construct by passing an x. Chen. The real catch here is that x may also pass a Promise instance when it is constructed using resolve or Reject, and it is also a promise instance that regulates other implementations, such as Bluebird or Q, so recursion is needed here. This hole is not stated in the specification, but there are many test cases that fail otherwise.
3.3 If x is a function or object
According to the specification:
- First take
let then = x.then
If an exception is thrown during this process, thereject(e)
- And then, if we did that in the last step
then
It’s a function- It is called with call, passing x as this to then, which takes the first argument to resolvePromise and the second argument to rejectPromise
- If resolvePromise is called with argument Y, then resolve_PROMISE is recursively called
- If the rejectPromise is called with the parameter r,
reject(r)
- If both resolvePromise and rejectPromise are invoked, the first one is ignored
- If an exception occurs when calling THEN from call
- If both resolvePromise and rejectPromise are invoked, ignore this exception
- If not, reject the exception
- It is called with call, passing x as this to then, which takes the first argument to resolvePromise and the second argument to rejectPromise
- If the then assigned in the first step is not a function, it is straightforward
resolve(x)
You may not be able to see what these Spaces are supposed to do in the specification, but it’s actually a compatible treatment for the Promise implementation. As we know, there is no official implementation of Promise, only specifications and test cases. There are various implementations of Promise, such as Bluebird and Q. If I use bluebird and Q at the same time, I need to do compatibility processing.
This x here could be an instance of Bluebird or Q.
So how do I know it’s a fulfillment of some other Promise? See if it has a THEN method. All Promise implementations have a prescribed THEN method. If there were then logic that would go here. If X were a normal object that contained the then method, it would also go here. That’s why there are so many judgments here, and recursion.
Just write MyPromise:
function resolve_promise(promise2, x, resolve, reject) {
// x is the case of promise2
if (x === promise2) {
reject(new TypeError('Chaining cycle detected for promise'))
return
}
// x is the case for MyPromise instances
if (x instanceof MyPromise) {
x.then(function(v) {
resolve_promise(promise2, v, resolve, reject)
}, function(t) {
reject(t)
})
return
}
// x is an object or a function
if(x ! = =null && (typeof x === 'function' || typeof x === 'object')) {
/ / switch
// Control calls to resolvePromise and rejectPromise and catch reject
let called = false
try { // x. teng may have an exception and needs to be caught
let then = x.then
if (typeof then === 'function') {
If the then method does not actually have a resolvePromise
// With the rejectPromise parameter, promise2 is always pending
// Resolve and reject are never executed
then.call(x, function resolvePromise(y) {
if (called) return
called = true
resolve_promise(promise2, y, resolve, reject)
}, function rejectPromise(r) {
if (called) return
called = true
reject(r)
})
} else {
Resolve if then is not a function
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}
Copy the code
Here, you can spot a Promise “bug” by looking at the code for the following scenario:
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > {
resolve('success')},1000)})let promise2 = promise1.then(function(res) {
// Return an object containing the then method
// But the then method does nothing
// This causes resolve or reject to never be implemented during construction
return {
then: function() {}
}
})
promise2.then(res= > {
// This is never executed
console.log(res)
})
// Promise2 is always pending
console.log(promise2)
Copy the code
You can try it by pasting the code above into your browser
3.4 If x is not a function or object
Resolve (x) is already there.
Correct a mistake
Look at the then function we have written:
MyPromise.prototype.then = function (onResolved, onRejected) {
let promise2
if (this.status === 'pending') {
promise2 = new MyPromise((resolve, reject) = > {
function successFn(value) {
let x = onResolved(value)
resolve_promise(promise2, x, resolve, reject)
}
function failFn(reason) {
let x = onRejected(reason)
resolve_promise(promise2, x, resolve, reject)
}
this.resolvedCallbacks.push(successFn)
this.rejectedCallbacks.push(failFn)
})
}
if (this.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) = > {
let x = onResolved(this.data)
// Look here, look at the bottom line
resolve_promise(promise2, x, resolve, reject)
})
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) = > {
let x = onRejected(this.reason)
// Look here, look at the bottom line
resolve_promise(promise2, x, resolve, reject)
})
}
return promise2
}
Copy the code
This. Status === ‘depressing’ and this. Status === ‘rejected’ fulfil this. So we need a setTimeout here. Also, according to the specification, onResolved and onRejected must be executed asynchronously.
Note that this.stauts === ‘pending’ is not needed because it executes asynchronously.
Change it to the following:
if (this.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) = > {
setTimeout((a)= > {
let x = onResolved(this.data)
resolve_promise(promise2, x, resolve, reject)
})
})
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) = > {
setTimeout((a)= > {
let x = onRejected(this.reason)
resolve_promise(promise2, x, resolve, reject)
})
})
}
Copy the code
Six, summarized
In fact, we’ve almost written a Promise now! But you might ask, promise.all promise.race instances don’t have catch methods yet! Don’t worry, as long as you complete then, a Promise will complete 80%, the above often several apis are trivial, to implement minutes ~ of course, it still has some minor flaws, these problems, and tests, we will complete the next article!
The completed code is here:
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = []
this.rejectedCallbacks = []
let resolve = (value) = > {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
this.resolvedCallbacks.forEach(fn= > fn(this.data))
}
}
let reject = (reason) = > {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
this.rejectedCallbacks.forEach(fn= > fn(this.reason))
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function (onResolved, onRejected) {
let promise2
if (this.status === 'pending') {
promise2 = new MyPromise((resolve, reject) = > {
function successFn(value) {
let x = onResolved(value)
resolve_promise(promise2, x, resolve, reject)
}
function failFn(reason) {
let x = onRejected(reason)
resolve_promise(promise2, x, resolve, reject)
}
this.resolvedCallbacks.push(successFn)
this.rejectedCallbacks.push(failFn)
})
}
if (this.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) = > {
setTimeout((a)= > {
let x = onResolved(this.data)
resolve_promise(promise2, x, resolve, reject)
})
})
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) = > {
setTimeout((a)= > {
let x = onRejected(this.reason)
resolve_promise(promise2, x, resolve, reject)
})
})
}
return promise2
}
function resolve_promise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(new TypeError('Chaining cycle detected for promise'))
return
}
if (x instanceof MyPromise) {
x.then(function(v) {
resolve_promise(promise2, v, resolve, reject)
}, function(t) {
reject(t)
})
return
}
if(x ! = =null && (typeof x === 'function' || typeof x === 'object')) {
let called = false
try {
let then = x.then
if (typeof then === 'function') {
then.call(x, function resolvePromise(y) {
if (called) return
called = true
resolve_promise(promise2, y, resolve, reject)
}, function rejectPromise(r) {
if (called) return
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}
Copy the code
Thank you for reading!