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 Pendingif(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 isfulfilledorrejected

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 takelet then = x.thenIf an exception is thrown during this process, thereject(e)
  • And then, if we did that in the last stepthenIt’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
  • If the then assigned in the first step is not a function, it is straightforwardresolve(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!