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

  1. 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.

  2. 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.

  3. 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