This is the 7th day of my participation in the August Text Challenge.More challenges in August

The Promise object is used to represent the final state (success/failure) of an asynchronous operation and its resulting value. – the MDN

The advent of Promises allowed us to gracefully handle asynchronous operations without the pain of callback hell. Every good has its bad, it becomes one of the interview questions, and turns into another kind of pain living around us… Of course, this is a joke ~ 🤣

The last time I saw Promise was about A month ago, and I spent two days in my spare time writing it by hand based on Promise/A+. It was cloudy and foggy. In order to consolidate while weekend wrote again, thoroughly comb clear ✌. This article is mainly dismantling the implementation of Promise beggar version and its periphery.

The basic characteristics of

  1. New promises are executed immediately and cannot be interrupted.
  2. Promise has three states:pengding,fulfilled,rejected. onlypengding -> fulfilledandpending -> rejectedTwo state flows, and cannot be changed again after the state changes.
  3. resolveIs the successful state;rejectIs in the failed state.

Promise basically uses 👇

const p = new Promise((resolve, reject) = > {
  resolve('success')
  reject('error')
})
p.then(value= > {
 console.log(value)
}, reason= > {
 console.log(reason)
})
// success
Copy the code

Dismantlement implement Promise beggar version

Implement basic logic

// promise.js
const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // The initial state is pengding
  this.status = PENGDING
  // Record the result value returned successfully
  this.value = null 
  // Record the result value returned by the failure
  this.reason = null
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
    }
  }
  executor(resolve, reject)
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    // The callback succeeded
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    // Call failed callback
    onRejected(this.reason)
  }
}
module.exports = Promise
Copy the code

Introduce the implementation of the beggar version Promise, perform below 🌰

// main.js
const Promise = require('./promise')
const p = new Promise((resolve, reject) = > {
  resolve('success')
  reject('error')
})
p.then(
  (value) = > {
    console.log(value)
  },
  (reason) = > {
    console.log(reason)
  }
)
Copy the code

Execute to print success.

Handling asynchronous cases

// main.js
const Promise = require('./promise')
const p = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('success')
  })
})
p.then(
  (value) = > {
    console.log(value)
  },
  (reason) = > {
    console.log(reason)
  }
)
Copy the code

This. Status of the then function is found to be pengding. Why is that? For those of you who know about event loops, setTimeout is asynchronous and is a macro task that will be executed in the next event loop macro task, and p.teng is the synchronization code for the current macro task. Therefore, thate. status is still pengding, so there is no output.

In this case, we need to add two fields ondepressing and onRejected to cache the successful and failed callbacks, and then execute the cache function resolve or Reject.

// promise.js
const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // The initial state is pengding
  this.status = PENGDING
  // Record the result value returned successfully
  this.value = null
  // Record the result value returned by the failure
  this.reason = null
  this.onFulfilled = null
  this.onRejected = null
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
      // If a cache callback succeeds, execute
      that.onFulfilled && that.onFulfilled(value)
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
      // There is a cache failure callback
      that.onRejected && that.onRejected(value)
    }
  }
  executor(resolve, reject)
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    // The callback succeeded
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    // Call failed callback
    onRejected(this.reason)
  } else {
    // Cache success and failure callback in mount state
    this.onFulfilled = onFulfilled
    this.onRejected = onRejected
  }
}
module.exports = Promise
Copy the code

After the improvement, execute the above asynchronous case code and print success successfully.

In the case of asynchrony, add more ingredients 🍕, execute different P. Chen functions many times.

// main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('success')
  })
})
p.then(
  (value) = > {
    console.log('First time', value)
  },
  (reason) = > {
    console.log(reason)
  }
)

p.then(
  (value) = > {
    console.log('The second time', value)
  },
  (reason) = > {
    console.log(reason)
  }
)
// The second success
Copy the code

Found that our first P.chen code was not executed. To analyze the reason: currently we only use one variable to store successful and failed callbacks. When I execute setTimeout, my two p.chen are executed in order. The second p.teng will override the callback assigned by the first p.teng, so the result is the second success.

To improve the code, we use arrays to store all callbacks.

const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // The initial state is pengding
  this.status = PENGDING
  // Record the result value returned successfully
  this.value = null
  // Record the result value returned by the failure
  this.reason = null
  // Array records all successful callbacks
  this.onFulfilled = []
  // Array records all failed callbacks
  this.onRejected = []
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
      // If a cache callback succeeds, execute
      that.onFulfilled.forEach((fn) = > fn(value))
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
      // There is a cache failure callback
      that.REJECTED.forEach((fn) = > fn(reason))
    }
  }
  executor(resolve, reject)
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    // The callback succeeded
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    // Call failed callback
    onRejected(this.reason)
  } else {
    // All success and failure callbacks are cached in mount state
    this.onFulfilled.push(onFulfilled)
    this.onRejected.push(onRejected)
  }
}
module.exports = Promise
Copy the code

Handling chain calls and value penetration (synchronization)

The focus of Promise is on chained invocation, which handles three cases. According to the Promise/A+ idea, each time A Promise. Then is executed, A new Promise is created and the return value of the previous THEN is passed to the then method of the next Promise to achieve chain calls and value penetration. The same applies to resolve and reject.

// main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) = > {
  // Only synchronous chain calls are currently handled
  resolve('success')})// Return the normal value
p.then((value) = > {
  return value
}).then((value) = > {
  console.log(value)
})
// Cannot read property 'then' of undefined

/ / return promise
p.then((value) = > {
  return new Promise((resolve, reject) = > {
     resolve(value)
  })
}).then((value) = > {
  console.log(value)
})
// Cannot read property 'then' of undefined

/ / value through
p.then().then((value) = > {
  console.log(value)
})
// Cannot read property 'then' of undefined

Copy the code

The main parts of the improved code are as follows.

.Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // Value penetration problem
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) = > value
  onRejected = typeof onRejected === 'function' ? onRejected : (reason) = > { throw reason }

  const p2 = new Promise((resolve, reject) = > {
    if (that.status === FULFILLED) {
      // The callback succeeded
      const x = onFulfilled(that.value)
      resolvePromise(x, resolve, reject)
    } else if (that.status === REJECTED) {
      // Call failed callback
      const x = onRejected(that.reason)
      resolvePromise(x, resolve, reject)
    } else {
      // All success and failure callbacks are cached in mount state
      that.onFulfilled.push(onFulfilled)
      that.onRejected.push(onRejected)
    }
  })
  return p2
}
function resolvePromise(x, resolve, reject) {
  // If x is a Promise object, execute its then function (callback of p2, not callback of x returned)
  if (x instanceof Promise) {
    x.then(
      (value) = > resolve(value),
      (reason) = > reject(reason)
    )
  } else {
    // If x is normal
    resolve(x)
  }
}
module.exports = Promise
Copy the code

Continuing with the code in all three of the above examples results in success.

  1. If the value is normal, directlyresolveThe return valuex.
  2. If it isPromise, the implementation ofx.then. Attention!!thenThe function pass parameter isp2theresolveandreject, so executeconst x = onFulfilled(that.value)Is executedp2theresolveFunction to pass the value along.
  3. Value penetrates at noresolveandrejectParameter to determine whether the input parameter is a function. If not, assign the default function.

Handling chain calls and value penetration (asynchronous)

Change the logic in main.js to asynchronous.

// main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('success')})})...Copy the code

First, p.teng and p.teng. Then are the first round of event loops, so they will be executed at setTimeout. Second, in the asynchronous case, we cache success and callback functions and wait for resolve or reject to execute. But we’re only caching the callback function, not a promise object, so we just need to wrap one more layer.

// promise.js
Promise.prototype.then = function (onFulfilled, onRejected) {...const p2 = new Promise((resolve, reject) = > {
    if (that.status === FULFILLED) {
      ...
    } else if (that.status === REJECTED) {
      ...
    } else {
      // All success and failure callbacks are cached in mount state
      that.onFulfilled.push(() = > {
        const x = onFulfilled(that.value)
        resolvePromise(x, resolve, reject)
      })
      that.onRejected.push(() = > {
        const x = onRejected(that.reason)
        resolvePromise(x, resolve, reject)
      })
    }
  })
  return p2
}
Copy the code

Handle the case of returning itself

Native promises throw Chaining cycle detected for promise #< promise > that returns errors equal to itself.

const p = new Promise((resolve, reject) = > {
   resolve('success')})const p1 = p.then((value) = > {
  console.log(value)
  return p1
})
Copy the code

ResolvePromise adds p2 pass-through and judgment

Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // Value penetration problem
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) = > value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) = > {
          throw reason
        }

  const p2 = new Promise((resolve, reject) = > {
    if (that.status === FULFILLED) {
      // The callback succeeded
      const x = onFulfilled(that.value)
      resolvePromise(p2, x, resolve, reject)
    } else if (that.status === REJECTED) {
      // Call failed callback
      const x = onRejected(that.reason)
      resolvePromise(p2, x, resolve, reject)
    } else {
      // All success and failure callbacks are cached in mount state
      that.onFulfilled.push(() = > {
        const x = onFulfilled(that.value)
        resolvePromise(p2, x, resolve, reject)
      })
      that.onRejected.push(() = > {
        const x = onRejected(that.reason)
        resolvePromise(p2, x, resolve, reject)
      })
    }
  })
  return p2
}
function resolvePromise(p2, x, resolve, reject) {
  // If return itself throws an error
  if (x === p2)
    reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  If x is a promise object
  if (x instanceof Promise) {
    x.then(
      (value) = > resolve(value),
      (reason) = > reject(reason)
    )
  } else {
    // If x is normal
    resolve(x)
  }
}
Copy the code

Perform errorp1 is not definedView stack error due toresolvePromise(p2, x, resolve, reject)In thep2Not initialized.

According to thepromise/A+The specification suggests that macro/micro tasks can be skillfully used to solve this problem, here I choosesetTimeoutThe form of macro tasks. The change code is as follows:

Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // Value penetration problem
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) = > value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) = > {
          throw reason
        }

  const p2 = new Promise((resolve, reject) = > {
    if (that.status === FULFILLED) {
      // The callback succeeded
      setTimeout(() = > {
        const x = onFulfilled(that.value)
        resolvePromise(p2, x, resolve, reject)
      })
    } else if (that.status === REJECTED) {
      // Call failed callback
      setTimeout(() = > {
        const x = onRejected(that.reason)
        resolvePromise(p2, x, resolve, reject)
      })
    } else {
      // All success and failure callbacks are cached in mount state
      that.onFulfilled.push(() = > {
        setTimeout(() = > {
          const x = onFulfilled(that.value)
          resolvePromise(p2, x, resolve, reject)
        })
      })
      that.onRejected.push(() = > {
        setTimeout(() = > {
          const x = onRejected(that.reason)
          resolvePromise(p2, x, resolve, reject)
        })
      })
    }
  })
  return p2
}
Copy the code

Add error handling

It mainly catches errors in constructors and then functions. Catch with try/catch

const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // The initial state is pengding
  this.status = PENGDING
  // Record the result value returned successfully
  this.value = null
  // Record the result value returned by the failure
  this.reason = null
  // Array records all successful callbacks
  this.onFulfilled = []
  // Array records all failed callbacks
  this.onRejected = []
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
      // If a cache callback succeeds, execute
      that.onFulfilled.forEach((fn) = > fn(value))
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
      // There is a cache failure callback
      that.onRejected.forEach((fn) = > fn(reason))
    }
  }
  The constructor caught an error
  try {
    executor(resolve, reject)
  } catch (err) {
    reject(err)
  }
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // Value penetration problem
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) = > value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) = > {
          throw reason
        }

  const p2 = new Promise((resolve, reject) = > {
    if (that.status === FULFILLED) {
      // The callback succeeded
      setTimeout(() = > {
        try {
          const x = onFulfilled(that.value)
          resolvePromise(p2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      })
    } else if (that.status === REJECTED) {
      // Call failed callback
      setTimeout(() = > {
        try {
          const x = onRejected(that.reason)
          resolvePromise(p2, x, resolve, reject)
        } catch (err) {
          reject(err)
       }
    } else {
      // All success and failure callbacks are cached in mount state
      that.onFulfilled.push(() = > {
        setTimeout(() = > {
          try {
            const x = onFulfilled(that.value)
            resolvePromise(p2, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        })
      })
      that.onRejected.push(() = > {
        setTimeout(() = > {
          try {
            const x = onRejected(that.reason)
            resolvePromise(p2, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        })
      })
    }
  })
  return p2
}
function resolvePromise(p2, x, resolve, reject) {
  // If return itself throws an error
  if (x === p2)
    reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  If x is a promise object
  if (x instanceof Promise) {
    x.then(
      (value) = > resolve(value),
      (reason) = > reject(reason)
    )
  } else {
    // If x is normal
    resolve(x)
  }
}
module.exports = Promise
Copy the code

Take 🌰 to verify

//main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) = > {
  throw new Error('failed')
})

p.then(
  (value) = > {
    console.log(value)
    return p1
  },
  (reason) = > {
    console.log(reason)
  }
)
// Error: failed
Copy the code

The standard version Promise

This version of the Promise has basically satisfied all cases, but always want to have A certificate, that also has to comply with our Promise/A+ specification.

  1. npm init
  2. npm install promises-aplus-tests --save-dev
  3. Methods to add packages
Promise.deferred = function () {
  var result = {}
  result.promise = new Promise(function (resolve, reject) {
    result.resolve = resolve
    result.reject = reject
  })

  return result
}
Copy the code
  1. package.jsonthescriptsThe field change command istest: promises-aplus-tests promiseAnd performnpm run test.

Execution, as expected, a bunch of errors. Change the resolvePromise function according to the specification as prompted

function resolvePromise(p2, x, resolve, reject) {
  // If return itself throws an error
  if (p2 === x) {
    reject(new TypeError('Chaining cycle detected for promise #<Promise>'))}// Execute x.chen ().
  if ((x && typeof x === 'object') | |typeof x === 'function') {
    // Avoid calling the same callback more than once
    let called = false
    try {
      let then = x.then
      if (typeof then === 'function') {
        then.call(
          x,
          (y) = > {
            if (called) return
            called = true
            resolvePromise(p2, y, resolve, reject)
          },
          (r) = > {
            if (called) return
            called = true
            reject(r)
          }
        )
      } else {
        // resolve(x)
        if (called) return
        called = true
        resolve(x)
      }
    } catch (err) {
      Error reject(err)
      if (called) return
      called = true
      reject(err)
    }
  } else {
    // if x is not an object or function (including if x is null)
    resolve(x)
  }
}
Copy the code

At this point, the standard Promise has changed.

Surrounding the Promise

Promise.resolve

Promise.resolve(value)

Returns a parsed Promise object for a given value.

  1. If the current value ispromiseObject, return thispromise.
  2. If it’s athenFunction, which takes its final state.
  3. Returns the current value as completedpromise.

Handwritten implementation

Promise.resolve = function (param) {
  if (param instanceof Promise) {
    return param
  }
  return new Promise((resolve, reject) = > {
    if (
      param &&
      typeof param === 'object' &&
      typeof param.then === 'function'
    ) {
      param.then(resolve, reject)
    } else {
      resolve(param)
    }
  })
}

Copy the code

Promise.reject

Return a Promise object with a reason for the rejection.

Basic implementation

Promise.reject = function(reason) {
  return new Promise((resolve, reject) = > {
    reject(reason)
  })
}
Copy the code

Promise.all

Promise.all(iterable)

Promise.all takes input from an iterable of promises (Array, Map, Set) and returns only one Promise instance. The instance’s resolve is executed after all the Promise’s resolve callbacks end. Reject is an error thrown immediately after any promise is executed.

The basic use

const p1 = 'promise'
const p2 = new Promise((resolve, reject) = > {
  resolve('success')})Promise.all([p1, p2].then((values) = > {
  console.log(values) // ['promise', 'success']
}))
Copy the code

Handwritten implementation

Promise.all = function (params) {
  const promises = Array.from(params)
  let values = []
  return new Promise((resolve, reject) = > {
    if(! params.length) resolve(values) promises.forEach((promise, index) = > {
      // Return values after all execution
      if (index === promises.length - 1) resolve(values)
      Promise.resolve(promise).then(
        (value) = > {
          console.log(value)
          values[index] = value
        },
        Reject any error, reject
        (reason) = > {
          reject(reason)
        }
      )
    })
  })
}
Copy the code

Promise.race

Returns a promise that is resolved or rejected once a promise in the iterator is resolved or rejected.

The basic use

const p1 = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('p1, success')},100)})const p2 = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('p2, success')},200)})Promise.race([p1, p2]).then((value) = > {
  console.log(value)
})
Copy the code

Handwritten implementation

Promise.race = function (params) {
  const promises = Array.from(params)
  return new Promise((resolve, reject) = > {
    if(! promises.length)return
    promises.forEach((promise) = > {
      Promise.resolve(promise).then(
        (value) = > {
          resolve(value)
          return
        },
        (reason) = > {
          reject(reason)
          return})})})}Copy the code

conclusion

If you feel helpful, don’t hesitate to go to 💕. In the next chapter, I will dig into the event cycle, add a concern with interest, and study together. 👀