From zero on, Promise goes from understanding to implementation

Promise is an object that represents the final completion or failure of an asynchronous operation.It has now become an important solution for asynchronous programming in JavaScript.

Before you get even closer to it, there are a few basic concepts to understand.

Alternatively, you can find the Here See the full source code.

Synchronous and asynchronous

Synchronization and asynchronization are often mentioned in the development process.

The so-called synchronization means that when a function call is made, the call must wait for the result to be returned, and the entire call will not end until the initiator of the call can continue to perform the subsequent operation.

function sayHello() {
  console.log("Hello world")
}
sayHello()
console.log("End of execution")
// Hello world
// End of execution

Asynchronous programming, on the other hand, can perform subsequent operations directly after invocation without waiting for the result to return.The callee notifies the caller by status, notification, or handles the result of the call through a callback function.

function sayHello() {
  console.log("Hello world")
}
setTimeout(sayHello)
console.log("End of execution")
// End of execution
// Hello world

Simply put, synchronization is a one-by-one completion, and you can't do the next until the previous one is done. Asynchronization is like publishing a task, and you can always publish it without worrying about its execution.

callback

So what are the callback functions mentioned above?

A callback function is a function that is called through a function pointer. Often, callbacks occur when we pass a function as a value through a parameter.

As mentioned above, one of the great uses of callbacks is for the caller to handle the execution results of asynchronous tasks.This is often seen in our development, especially in Node.js.

const fs = require("fs")

fs.readFile("input.txt", function(err, data) {
  if (err) return console.error(err)
  console.log(data.toString())
})

console.log("End of execution")

For the above concepts of synchronization and asynchronization, callbacks can also be divided into synchronous callbacks and asynchronous callbacks.

By definition, synchronous callbacks are called synchronously, such as forEach, map on the array prototype; asynchronous callbacks are also common, such as DOM events, timer functions, and so on.

Callback to Hell

Next, the function of callbacks is that when we want to use the results for further processing after some asynchronous tasks have been completed, the most direct and simple way is to use callbacks, which is good and does solve a lot of problems to some extent.

What further?If we have some asynchronous operations in the callback function and need to do something with this asynchronous result, we need to add another layer of callbacks.

Maybe that's okay, because it's two levels too, but what about the deeper levels that would result if you followed this cycle?

Undoubtedly, it's a nightmare, so we call this situation "parent (pit)" a callback to hell.

It, like the one below, can be even more complex.

fs.readdir(source, function(err, files) {
  if (err) {
    console.log("Error finding files: " + err)
  } else {
    files.forEach(function(filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function(err, values) {
        if (err) {
          console.log("Error identifying file size: " + err)
        } else {
          console.log(filename + " : " + values)
          aspect = values.width / values.height
          widths.forEach(
            function(width, widthIndex) {
              height = Math.round(width / aspect)
              console.log(
                "resizing " + filename + "to " + height + "x" + height
              )
              this.resize(width, height).write(
                dest + "w" + width + "_" + filename,
                function(err) {
                  if (err) console.log("Error writing file: " + err)
                }
              )
            }.bind(this)
          )
        }
      })
    })
  }
})

More descriptions can be clicked to view Callback Hell.

Fortunately, now we can use Promise to avoid calling back to Hell.

Move Up One Level

Let's start with an asynchronous example, which contains some simple asynchronous operations and specifies the corresponding callback functions, which describe the solution of traditional asynchronous programming very well.

function successCb(value) {
  console.log(value)
  // [async] code ...
}

function failedCb(reason) {
  console.log(reason)
  // [async] code ...
}

function toDo(successCb, failedCb) {
  // other code ...
  setTimeout(() => {
    if (Math.random() > 0.5) {
      successCb(0)
    } else {
      failedCb(1)
    }
  }, 1000)
}

toDo(successCb, failedCb)

Next, let's just get to know Promise.

Promise is a new asynchronous programming solution provided by JavaScript, and one notable example is resolving the callback hell described above.

Specifically, Promise is an object (constructor called through new) that encapsulates an asynchronous operation through which messages for asynchronous operations can be obtained.

The Promise constructor always takes a function as a parameter and provides two parameters, resolve and reject, for this function, both of which are functions used to change state and return values.

There are three states for a Promise object: pending, fulfilled, and rejected.

It is important to note that a change in state can only change from an ongoing state to another, and once it changes, it will not change again.

Promise is initially pending, in which the asynchronous operation results in a state that can be converted to fulfilled by the resolve function or rejected by a call to reject.

We can then process it externally using the then method on the Promise instance.

Next is a simple example of use, a modification to the above example.

new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve(0)
    } else {
      reject(1)
    }
  }, 1000)
}).then(
  value => console.log(value),
  reason => console.log(reason)
)

In fact, it can be seen that the then method encapsulates the steps in which we pass the callback function, but the traditional callback function can only be passed in when the task is executed, whereas the Promise method can be specified later.

Also, it is important to note that the then method returns a new Promise, so we can make chain calls, which is the key to resolving callback hell.

Start implementing

The use of Promise, not to mention much here, focuses on how to do it next.

Before adding bricks and tiles, we first lay out the cornerstones, listing the entire structure in a custom class, including the prototype and instance methods.

// Identity defining three states
const PENDING = "PENDING"
const FULFILLED = "FULFILLED"
const REJECTED = "REJECTED"

class _Promise {
  constructor(executor) {
    // Define basic instance properties
    this.$$status = PENDING
    this.$$value = undefined // Value of success or reason for failure
    this.$$callbacks = []
  }
  resolve(value) {}
  reject(reason) {}
  then(onResolved, onRejected) {}
  catch(onRejected) {}
  finally(onFinally) {}
  static all(iterable) {}
  static race(iterable) {}
  static resolve(value) {}
  static reject(reason) {}
}

Then, to further refine the constructor, which now initializes information such as state and uses an array to store future registered callback functions, it is time to execute the incoming executor.

Before calling the executor, you need to implement two prototype auxiliary functions (resolve, reject), which are responsible for changing state and calling the corresponding callback function.

These callback functions are specified by the then method. To distinguish between successful callbacks and failed callbacks, we store them in the data structure of the object, so we use a simplified implementation here first.

// ...
class _Promise {
  // ...
  then(onResolved, onRejected) {
    this.$$callbacks.push({ onResolved, onRejected })
  }
  // ...
}

The real then method is not that simple, which will be detailed in the subsequent implementation. This is just to let us continue better. Now let's go on to see the implementation of the two auxiliary functions mentioned above.

// ...
class _Promise {
  // ...
  resolve(value) {
    // State can only be changed once
    if (this.$$status !== PENDING) {
      return
    }
    this.$$value = value
    this.$$status = FULFILLED
    if (!this.$$callbacks.length) {
      return
    }
    // Callback functions registered with then are all executed asynchronously
    setTimeout(() => {
      this.$$callbacks.forEach(cbMap => {
        cbMap.onResolved(value)
      })
    })
  }
  reject(reason) {}
  // ...
}

We changed the state in the resolve method and detected the state before that. Once the state changed, no further operations will be performed or the corresponding callback function will be called.

The implementation of another reject method is almost identical.

// ...
class _Promise {
  // ...
  resolve(value) {
    /* ... */
  }
  reject(reason) {
    if (this.$$status !== PENDING) {
      return
    }
    this.$$value = reason
    this.$$status = REJECTED
    if (!this.$$callbacks.length) {
      return
    }
    setTimeout(() => {
      this.$$callbacks.forEach(cbMap => {
        cbMap.onRejected(reason)
      })
    })
  }
  // ...
}

Now let's go back to the constructor and start calling the executor.Since the executor may fail to execute, we need to catch possible errors and handle them as failures.

class _Promise {
  // ...
  constructor(executor) {
    this.$$status = PENDING
    this.$$value = undefined
    this.$$callbacks = []

    try {
      executor(this.resolve.bind(this), this.reject.bind(this))
    } catch (err) {
      this.reject(err)
    }
  }
  // ...
}

Since the resolve method and reject will be passed as parameters, we have hard-bound their this before passing them, in addition to that, at a glance.

At this point, our simple version of Proise is ready, and if it works correctly, the code below will work very well.

new _Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve(0)
    } else {
      reject(1)
    }
  }, 1000)
}).then(
  value => console.log(value),
  reason => console.log(reason)
)

then

The above three functions are easy to implement and easy to understand, but the implementation of the then method is relatively tricky, and it is also the core step of the entire implementation.

Next to the example above, if we remove asynchronization from the executor, the result will not have any output.

new _Promise((resolve, reject) => {
  if (Math.random() > 0.5) {
    resolve(0)
  } else {
    reject(1)
  }
}).then(
  value => console.log(value),
  reason => console.log(reason)
)

Why is that so?Since the executor executes synchronously, the state has changed when we added the callback function through the then method.

In the current implementation, when the state changes, there is no place to call the callback function anymore, which is obviously incorrect, and we have not returned the new Promise correctly.

So now we need to return to a new Promise first, and in this new Promise we need to decide how to handle the callback function being registered based on the current Promise status:

  • When the state is PENDING, the callback function is stored in the callback array.
  • When the state has changed to FULFILLED, the callback for asynchronous invocation succeeds;
  • A callback that fails an asynchronous call when the state has changed to REJECTED.
// ...
class _Promise {
  // ...
  then(onResolved, onRejected) {
    return new Promise((resolve, reject) => {
      if (this.$$status === PENDING) {
        this.$$callbacks.push({ onResolved, onRejected })
      } else if (this.$$status === FULFILLED) {
        setTimeout(() => onResolved(this.$$value))
      } else {
        setTimeout(() => onRejected(this.$$value))
      }
    })
  }
  // ...
}

It looks a lot better, but one important feature is not implemented: the status of the newly returned Promise is determined by the results of the callback execution, and we are not trying to change the status of the newly returned Promise at this time.

So how does the result of the callback affect the status of the newly returned Promise?This includes three scenarios:

  • When the callback executes an exception, the state of the newly returned Promise changes to REJECTED, and reason is the error message.
  • When the value returned by the callback is not Promise, the state of the newly returned Promise changes to FULFILLED, and value is the value returned by the callback.
  • When the return value of the callback is Promise, the status of the newly returned Promise is determined by the Promise returned by the callback.

Now put the callback into the try...catch statement to catch possible errors, and use a variable to accept the value returned by the callback, depending on whether it is a Promise or not.

// ...
class _Promise {
  // ...
  then(onResolved, onRejected) {
    return new Promise((resolve, reject) => {
      if (this.$$status === PENDING) {
        // ...
      } else if (this.$$status === FULFILLED) {
        setTimeout(() => {
          try {
            const ret = onResolved(this.$$value)
            if (ret instanceof _Promise) {
              // If the execution results return a promise
              // Then the status of the newly returned promise is determined by the returned promise
              ret.then(resolve, reject)
            } else {
              resolve(ret)
            }
          } catch (err) {
            // Fail if an error occurs
            reject(err)
          }
        })
      } else {
        // ...
      }
    })
  }
  // ...
}

The asynchronous execution of a bad callback is exactly the same as the asynchronous execution of a successful callback above, except that the callback invoked needs to be changed to onRejected.

So now we've handled how to modify the state of the newly returned Promise based on the callback execution results in the case of success and failure, but what if it was PENDING?

We are currently just placing callbacks directly in the callback queue. It is true that it can be executed when the current Promise state changes, but it has no relation to the new Promise we are returning, so we need to wrap the callbacks before they can be added to the queue.

It is also simple to place the specified callback in a custom function (which is exactly the same as what we handled above for asynchronous execution success/error callbacks), and then add the custom function to the callback queue.

// ...
class _Promise {
  // ...
  then(onResolved, onRejected) {
    return new Promise((resolve, reject) => {
      if (this.$$status === PENDING) {
        this.$$callbacks.push({
          onResolved: () => {
            try {
              const ret = onResolved(this.$$value)

              // After wrapping we invoked resolve or reject methods as appropriate
              if (ret instanceof _Promise) {
                ret.then(resolve, reject)
              } else {
                resolve(ret)
              }
            } catch (err) {
              reject(err)
            }
          },
          onRejected: () => {
            try {
              const ret = onRejected(this.$$value)

              if (ret instanceof _Promise) {
                ret.then(resolve, reject)
              } else {
                resolve(ret)
              }
            } catch (err) {
              reject(err)
            }
          }
        })
      } else if (this.$$status === FULFILLED) {
        setTimeout(() => {
          try {
            const ret = onResolved(this.$$value)

            if (ret instanceof _Promise) {
              // If the execution results return a promise
              // Then the status of the newly returned promise is determined by the returned promise
              ret.then(resolve, reject)
            } else {
              resolve(ret)
            }
          } catch (err) {
            reject(err)
          }
        })
      } else {
        setTimeout(() => {
          try {
            const ret = onRejected(this.$$value)

            if (ret instanceof _Promise) {
              ret.then(resolve, reject)
            } else {
              resolve(ret)
            }
          } catch (err) {
            reject(err)
          }
        })
      }
    })
  }
  // ...
}

Fortunately, it handles both synchronous and asynchronous state changes very well and correctly returns to the new Promise, but the code seems redundant and redundant, so let's make a few changes now.

// ...
class _Promise {
  // ...
  then(onResolved, onRejected) {
    return new _Promise((resolve, reject) => {
      const handler = cb => {
        try {
          const ret = cb(this.$$value)
          if (ret instanceof _Promise) {
            ret.then(resolve, reject)
          } else {
            resolve(ret)
          }
        } catch (err) {
          reject(err)
        }
      }

      if (this.$$status === PENDING) {
        this.$$callbacks.push({
          onResolved: () => handler(onResolved),
          onRejected: () => handler(onRejected)
        })
      } else if (this.$$status === FULFILLED) {
        // Keep in mind that callbacks registered with the then execute asynchronously
        setTimeout(() => {
          handler(onResolved)
        })
      } else {
        setTimeout(() => {
          handler(onRejected)
        })
      }
    })
  }
  // ...
}

It looks much better now, but we know that the then method can only specify a successful callback, and we directly default to receiving two function parameters, so we need to do some processing on the parameters.

At the same time, to achieve the effect of error propagation, if no wrong callback is specified, then the default callback is specified to pass the error.

// ...
class _Promise {
  // ...
  then(onResolved, onRejected) {
    onResolved = typeof onResolved === 'function' ? onResolved : value => value
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : reason => {
            // Error propagation
            throw reason
          }

    return new _Promise((resolve, reject) => {/* ... */}
  }
  // ...
}

catch

Finally, we have implemented the core method then, and the next few methods are relatively simple.

The catch method adds a rejection callback to the current Promise and returns a new Promise.

In fact, the catch method can be thought of as a then method that specifies only error callbacks.

// ...
class _Promise {
  // ...
  catch(onRejected) {
    return this.then(undefined, onRejected)
  }
  // ...
}

There is also a final method associated with catch, where the implementation refers to Miss Ruan's implementation, so we won't go into much detail.

reject

We've already implemented reject as an auxiliary method on the prototype, but now we need to implement a static method with the same name on Pormise.

The reject method returns a Promise object with a status of failure and passes the given failure information to the corresponding processing method.

Based on our previous implementation, if you need a failed Promise, you only need to call the reject method on its prototype after creating the Promise object.

// ...
class _Promise {
  // ...
  static reject(reason) {
    return new _Promise((resolve, reject) => {
      reject(reason)
    })
  }
  // ...
}

resolve

Corresponding to the reject method, the resolve method returns a Promise object whose state is determined by the given value.

If the value is empty, the base type, or an object without the then method, the Promise object returned is fulfilled and the value is passed to the corresponding then method.

If the value is thenable (that is, an object with the then method), the final state of the Promise object returned is determined by the then method execution.

Therefore, we need to process differently based on the value passed.

// ...
class _Promise {
  // ...
  static resolve(value) {
    return new _Promise((resolve, reject) => {
      // First determine if the value is an object
      if (
        (typeof value === "object" && value !== null) ||
        typeof value === "function"
      ) {
        try {
          const then = value.then
          // Next, further determine if the value is thenable
          if (typeof then === "function") {
            then(resolve, reject)
          } else {
            // If it's not the nable, it's a normal object
            // Then pass the value directly to the corresponding then method
            resolve(value)
          }
        } catch (err) {
          reject(err)
        }
      } else {
        // Ordinary value if not an object
        // Then pass the value directly to the corresponding then method
        resolve(value)
      }
    })
  }
  // ...
}

Since the then method on the object may be customized, errors may occur during the call, so we put the code for the part of the call in the try..catch statement and return it as the cause of the failure if an error is caught.

Generally, if you do not know if a value is a Promise object, you can use Promise.resolve(value) to return a Promise so that you can use the value as a Promise object.

all

The Promise.all() method accepts an iterative object as a parameter and returns a Promise instance based on that object.

Each item in the iteration object is usually an instance of Promise, and if not, the Promise.resolve method is invoked to turn it into a Promise instance.

The state of the last returned instance will not change to successful until all Promises in the passed parameters are successful, and the returned values of each Promise will be returned in an array in the order in which they were passed.

If one of the Promises fails during the entire wait, the returned Promise becomes a direct failure.

// ...
class _Promise {
  // ...
  static all(iterable) {
    return new _Promise((resolve, reject) => {
      let i = 0 // Number of successful Promise s
      const len = iterable.length
      const values = []

      if (!len) {
        resolve(values)
        return
      }

      // Store successful values in the eventually returned array in the passed location
      function emitValues(index, value) {
        values[index] = value
        if (++i === len) {
          resolve(values)
        }
      }

      iterable.forEach((item, index) => {
        if (item instanceof _Promise) {
          item.then(curValue => emitValues(index, curValue), reject)
        } else {
          emitValues(index, item)
        }
      })
    })
  }
  // ...
}

It is important to note that we save each item's location through index during traversal, store the corresponding value according to this location after the state changes, and change the Promise that is returned when the number of successes i is equal to the number passed in.

race

The Promise.race() method is much simpler than Promise.all().

The Promise.race() method also accepts an iterable as a parameter and returns an instance of Promise.

When any of the child Promises in the iterable parameter succeeds or fails, the parent Promise immediately invokes the appropriate handle to the parent Promise binding using the success return value or failure details of the child Promise as an argument.

// ...
class _Promise {
  // ...
  static race(iterable) {
    return _Promise((resolve, reject) => {
      iterable.forEach(item => {
        if (item instanceof _Promise) {
          item.then(resolve, reject)
        } else {
          resolve(item)
        }
      })
    })
  }
  // ...
}

Now it's done.

Other

The implementation written here is just to provide a way of thinking, welcome you to exchange and learn together.

Tap [Funny].

link

Keywords: Javascript Programming ECMAScript

Added by NDK1971 on Mon, 16 Dec 2019 02:10:08 +0200