Write Your Own Node.js Promise Library from Scratch

Author: code_barbarian

Write a Promise library from scratch

Promise is already a cornerstone of asynchronous processing in JavaScript, callback scenarios will be less and less common, and async/await can now be used directly in Node.js. Async /await is based on promises, so you need to know promises to master async/await. This article will show you how to write a Promise library and demonstrate how to use async/await.

What is Promise?

In the ES6 specification, a Promise is a class whose constructor accepts an executor function. An instance of the Promise class has a THEN () method. Promise also has some other properties according to the specification, but we can ignore them here because we’re implementing a stripped-down version of the library. Here is a scaffolding of the MyPromise class:

The class MyPromise {// 'executor' function takes two arguments, 'resolve()' and 'reject()' // Be responsible for calling 'resolve()' or 'reject()' constructor(executor) {} when the asynchronous operation succeeds (resolved) or fails (rejected) This will be a big pity when the state of the promise is fulfilled. // This will be a big pity when the state of the promise is fulfilled. // This will be a big pity when the state of the promise is fulfilled'fulfilled''resolved'Is the samethen(onFulfilled, onRejected) {}
}
Copy the code

The executor function takes two arguments, resolve() and reject(). Promise is a state machine that contains three states:

  • Pending: The initial state, which is neither successful nor failed
  • Depressing: means that the operation is completed successfully and the result value will be returned
  • Rejected: Indicates that the operation fails and an error message is returned

This makes it easy to implement the initial version of the MyPromise constructor:

constructor(executor) {
    if(typeof executor ! = ='function') {
        throw new Error('Executor must be a function'} // Initial state,$stateRepresents the current state of the promise //$chainedIs the array of functions called this when a Promise is in the Texas state.$state = 'PENDING'
    this.$chained= [] // Implement 'resolve()' and 'reject()' const resolve => {// as long as' resolve() 'or' reject() 'is called // this promise The object is no longer in a pending state, which is termed settled. // Calling 'resolve()' or 'reject()' twice and calling 'reject()' after 'resolve()' is invalidif (this.$state! = ='PENDING') {
            return} // There is a subtle difference between Regrettable and resolved this.$state = 'FULFILLED'
        this.$internalValue = res
        // If somebody called `.then()` while this promise was pending, need
        // to call their `onFulfilled()` function
        for (const { onFulfilled } of this.$chained) {
            onFulfilled(res)
        }
    }
    const reject = err => {
        if (this.$state! = ='PENDING') {
            return
        }
        this.$state = 'REJECTED'
        this.$internalValue = err
        for (const { onRejected } of this.$chained) {onRejected(err)}} // As the specification says, call the 'resolve()' and 'reject()' try {// If the processor function throws a synchronization error, we consider this to be a failed state. 'resolve()' and 'reject()' can only be called once executor(resolve, reject)} catch (err) {reject(err)}}Copy the code

The implementation of the then() function is simpler. It accepts two arguments, onFulfilled() and onRejected(). The then() function must ensure that the promise calls onFulfilled() when fulfilled and onRejected() when fulfilled. If the promise is resolved or Rejected, the then() function will immediately call onFulfilled() or onRejected(). If the promise is still pending, the function is pushed into the $chained array, so the subsequent resolve() and Reject () functions can still call them.

then(onFulfilled, onRejected) {
    if (this.$state= = ='FULFILLED') {
        onFulfilled(this.$internalValue)}else if (this.$state= = ='REJECTED') {
        onRejected(this.$internalValue)}else {
        this.$chained.push({ onFulfilled, onRejected })
    }
}
Copy the code

* This is a pity () or onFulfilled() which will be called in the next sequence if.then() is called in the promise which is already resolved or rejected. Because the code in this article is an instructional example rather than an exact implementation of the specification, the implementation ignores these details.

Promise invocation chain

The above example deliberately ignores the most complex and useful part of promise: the chained invocation. If onFulfilled() or onRejected() returns a promise, then() should return a new promise that is “locked in” to match the promise state. Such as:

p = new MyPromise(resolve => {
    setTimeout(() => resolve('World'), 100)
})

p
    .then(res => new MyPromise(resolve => resolve(`Hello, ${res}'))) // Prints after 100 ms'Hello, World'
    .then(res => console.log(res))
Copy the code

The following is an implementation of the.then() function that returns a promise so that the chain call can be made.

then(onFulfilled, onRejected) {
    returnnew MyPromise((resolve, // Make sure that the error in 'onFulfilled()' and 'onRejected()' will lead to the return promise failure (reject) const _onFulfilled => {try {// This is a big pity if 'onFulfilled()' returns a promise, Resolve (onpity (res))} catch (err) {reject(err)}} const _onRejected = err => {try {this is a big pity. reject(onRejected(err)) } catch (_err) { reject(_err) } }if (this.$state= = ='FULFILLED') {
            _onFulfilled(this.$internalValue)}else if (this.$state= = ='REJECTED') {
            _onRejected(this.$internalValue)}else {
            this.$chained.push({ onFulfilled: _onFulfilled, onRejected: _onRejected })
        }
    })
}
Copy the code

Now then() returns a promise, but there is still some work to be done: if ondepressing () returns a promise, resolve() needs to be fulfilled correctly. So resolve() needs to be called recursively from then(), and here’s the updated resolve() function:

Const resolve = res => {// As long as' resolve() 'or' reject() 'is called // the promise object is no longer pending, Call 'resolve()' or 'reject()' twice, and 'reject()' after 'resolve()' is invalidif (this.$state! = ='PENDING') {
        return} // If 'res' is thenable (withthenMethod) // locks the promise to keep it in the same state as Thenableif(res ! == null && typeof res.then ==='function'{// In this case, the promise is resolved, but is still in'PENDING'State // This is what the ES6 specification says"An resolved promise.", may be in the pending, depressing or rejected state // http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects
        return res.then(resolve, reject)
    }

    this.$state = 'FULFILLED'
    this.$internalValue = res
    // If somebody called `.then()` while this promise was pending, need
    // to call their `onFulfilled()` function
    for (const { onFulfilled } of this.$chained) {
        onFulfilled(res)
    }

    return res
}
Copy the code

For simplicity, the above example omits the key detail that calls to resolve() or reject() are invalid once a promise is locked to match another promise. In the above example, you can resolve() a pending promise, then throw an error, and res.then(resolve, reject) will be invalid. This is just an example, not a full implementation of the ES6 Promise specification.

The code above illustrates the difference between resolved promises and fulfilled promises. The difference is subtle and has to do with the promise chain call. Resolved is not a true promise state, but it is the term defined in the ES6 specification. When you call resolve() on an already resolved promise, one of two things can happen:

  • In the callresolve(v)If thevIs not a promise, then the promise will immediately become a pity. In this simple situation, Resolved and Fulfilled are the same.
  • In the callresolve(v)If thevIs another promise, so this promise is always pending untilvCall resolve or reject. In this case, the promise is resolved but in a pending state.

Use with Async/Await

The keyword await suspends execution of an async function until the waiting promise becomes settled. Now that we have a simple homemade promise library, let’s see what happens when we use async/await in combination. Add a console.log() statement to the then() function:

then(onFulfilled, onRejected) {
    console.log('Then', onFulfilled, onRejected, new Error().stack)
    returnnew MyPromise((resolve, reject) => { /* ... * /})}Copy the code

Now let’s await an instance of MyPromise and see what happens.

run().catch(error => console.error(error.stack))

async function run() {
    const start = Date.now()
    await new MyPromise(resolve => setTimeout(() => resolve(), 100))
    console.log('Elapsed time', Date.now() - start)
}
Copy the code

Notice the.catch() call above. The catch() function is a core part of the ES6 Promise specification. This article won’t go into detail about it, because.catch(f) is equivalent to.then(null, f) and there’s nothing special about it.

This is the ondepressing () and onRejected() functions in.then(), which is V8’s C++ native code. Furthermore, await will wait until.then() is called until the next sequence.

Then function () { [native code] } function () { [native code] } Error
    at MyPromise.then (/home/val/test/promise.js:63:50)
    at process._tickCallback (internal/process/next_tick.js:188:7)
    at Function.Module.runMain (module.js:686:11)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3
Elapsed time 102
Copy the code

More and more

Async /await is a very powerful feature, but it’s a little harder to master because it requires the user to understand the basic principles of promise. Promises have many details, such as catching synchronization errors in processor functions and not being able to change state once a promise is resolved, which makes async/await possible. Once you have a full understanding of promises, async/await becomes much easier.