Promise origin

Before promises, the main way to solve asynchronous problems was through callback nesting, which would fetch the result of asynchronous execution after the execution of asynchronous logic. Layer upon layer of nested callback can make the code logic unintuitive and bad for later development and maintenance.

fs.readFile("./a.txt".(err, data) = > {
  fs.readFile(data, (err, data) = > {
  		fs.readFile(data, (err, data) = > {
                    // Call back the black hole})})})Copy the code

Promise subtly transforms the nesting of asynchronous callbacks into a chain of synchronous callbacks. Essentially, promises internally use callback functions to solve the asynchronous problem, but they can be expressed in a synchronous way.

new Promise((reslove, reject) = > {
  // Asynchronous logic
}).then(
// Asynchronous logic
).then(
// Asynchronous logic
).then(

)
Copy the code

A synchronous version of Promise

Directly on the Promise source code, the source code of Promise is based on the Promise/A+ specification to achieve

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected'

function Promise(executor) {
    /** * The Promise object has an internal state state, which defaults to an initial state pending. * The state changes according to the result of the Promise execution, and the state can only change once throughout the process. * Switched from "waiting pending" to "successful Resolved", or from "waiting" to "failed"; * * /
    this.state = PENDING;
    this.value = undefined;
    this.reason = undefined;

    let resolve = (value) = > {
         // The state can be changed only when the current state is pending
        if (this.state === PENDING) {
            this.state = RESOLVED;
            this.value = value; }}let reject = (reason) = > {
        if (this.state === PENDING) {
            this.state = REJECTED;
            this.reason = reason; }}/** * Promise takes a function as an argument that will be executed immediately on new Promise, and it takes two functions as arguments that will call the first resolve() argument if the Promise operation succeeds. * The second argument is called reject() */ when the Promise operation fails
    try {
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

Promise.prototype.then = function (onResolved, onRejected) {
  /** * Each generated Promise instance object has the then method, which also takes two function operation parameters. * When calling the THEN method, if the state is Resolved, the first onResolved function will be called. * Execute the second function onRejected */ when the rejected state is in the rejected state
    if (this.state === RESOLVED) {
        onResolved(this.value)
    }

    if (this.state === REJECTED) {
        onRejected(this.reason)
    }
}
Copy the code

But the Promise is a basic version that only addresses the synchronization problem, because when the generated Promise object calls the THEN method, the state has changed and the corresponding logic can be executed. However, consider that if an asynchronous operation is performed within an executor function, Therefore, when the generated Promise instance is called then, the state of resolve and reject are not called yet, so the state state is still pending. In our then method above, there is no processing logic for pending state. This is where asynchronous problem handling comes in.

If we call the THEN method and its internal state is pending, we can store the two function parameters passed to the THEN method. After the asynchronous operation is complete, when either of the two function arguments received by executor is called, go back and execute the method that was held in then:

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected'

function Promise(executor) {

    this.state = PENDING;
    this.value = undefined;
    this.reason = undefined;
    // Store the first argument to then, that is, the callback function to be called when the state is success
    this.onResolvedCallbacks = []; 
    // Hold the second argument to then, that is, the callback function to be called when the state is failed
    this.onRejectedCallbacks = []; 

    let resolve = (value) = > {
        if (this.state === PENDING) {
            this.state = RESOLVED;
            this.value = value;
            // Execute the onResolved function within then when resolve is called, that is, when the Promise state is successful
            this.onRejectedCallbacks.forEach(fn= > fn())
        }
    }

    let reject = (reason) = > {
        if (this.state === PENDING) {
            this.state = REJECTED;
            this.reason = reason;
            // Call REJECT, which calls the onRejected function in THEN when the Promise state fails
            this.onRejectedCallbacks.forEach(fn= > fn())
        }
    }

    try {
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

Promise.prototype.then = function (onResolved, onRejected) {
    if (this.state === RESOLVED) {
        onResolved(this.value)
    }

    if (this.state === REJECTED) {
        onRejected(this.reason)
    }
    /** ** for asynchronous operations, the state must be waiting for the first time, **/ after the asynchronous operation is complete, then back to execute **/
    if (this.state === PENDING) {
        this.onResolvedCallbacks.push(onResolved)
        this.onRejectedCallbacks.push(onRejected)
    }
}
Copy the code

You might have some questions here

  1. OnResolvedCallbacks and onRejectedCallbacks are received in arrays. Originally a function, directly use a variable to save the line?

    /** * The same PROMISE object's THEN methods can be called more than once. * When the THEN methods are called more than once, the tasks within each THEN method need to be received */
    var promise = new Promise(/ * * /)
    promise.then()
    promise.then()
    Copy the code
  2. Why is the state of a synchronous operation changed while the state of an asynchronous operation is pending when the execute THEN method is called?

    var p = new Promise((resolve, reject) = > { /* Block 1 */})
    p.then(/* Block 2*/)
    Copy the code

    When a Promise is called with the new operator, block 1 is executed, then an object P is generated, and block 2 is executed when p calls then. If the operations in the above code block are synchronous, there is no suspense, and the code executes from top to bottom. However, when block 1 is an asynchronous operation, the execution sequence of the JS engine is to execute the current synchronous code before removing the asynchronous code. Therefore, if block 1 is an asynchronous operation, the state has not changed when calling the THEN method.

Go on, we’re not done… What follows is the important point.

The chain calls of promises and the penetrative nature of the values of then methods

What we found when we used promises was that their THEN methods kept making chain calls, and that the return values of the function parameters in the previous THEN method could be retrieved in the next THEN method, or even used in the next THEN method. How did this work?

The chain invocation is simple. We can implement the chain invocation by returning a promise object when calling the THEN method. For the value penetration of the THEN method, if you can call the THEN method and return a new promise, you can pass the return value of the then parameter function to the new promise object when the new promise object calls its resolve/ Reject method This can be used in the then of a newly returned Promise object…… Let’s go straight to the code; Since the Promise constructor code is the same as above, I won’t copy it over here without changing it. The main changes here are the then function and the resolvePromise function that handles the value.

According to the Promise/A+ specification, A Promise must provide A THEN function that deals with its value or reason, and this THEN function accepts two optional functions as parameters of the Promise. Then (onFulfilled, onRejected)

  1. If ondepressing or onRejected is not a function, then simply ignore it

  2. If ondepressing is a function

    It must be called after the Promise state is Resolved and take the Promise’s value as its first argument

    It will not be called until the state of the Promise is Resolved

    It will only be called once

  3. If onRejected is a function

    It must be called after the Promise state rejected and take the Promise’s Reason value as its first argument

    It will not be called until the promise state is Rejected

    It will only be called once

  4. OnFulfilled or onRejected will be called only after the code in the current execution context is completed.

  5. Ondepressing or onRejected must be called as a function

  6. The then function may be called multiple times by the same Promise object

    If the state of the promise is Resolved, all the respective onFulfilled methods will be called and executed in the same order as calling THEN

    When promise is rejected, all respective onFejected methods will be invoked in the order they called THEN

  7. The then function must return a promise: promise2 = promise1. Then (onFulfilled, onRejected)

    If onFulfilled or onRejected returns a value x, then perform resolvePromise(promise2, x)

    If onFulfilled or onRejected throws an exception “E”,promise2 must become the “Rejected” state and treat this “E” as reason

    If onFulfilled is not a function and promise1 is resolved,promise2 must become resolved and use the value of promise1 as the value of promise2

    If ondepressing is not a function and promise1 is the failed state,promise2 must also become the rejected state and use the reason of promise1 as the reason of promise2

Promise.prototype.then = function (onResolved, onRejected) {
  /** * According to the Promise/A+ specification, the two argument functions received by the THEN method are optional, and will be ignored, replaced with an empty function, or thrown an exception if it is not A function
    onResolved = typeof onResolved === 'function' ? onResolved : v= > v
    onRejected = typeof onRejected === 'function' ? onRejected : err= > {throw err }
    let promise2 = new Promise((resolve, reject) = > {
        if (this.state === RESOLVED) {
          /** * If the resolvePromise function returns a value of undefined, the Promise function returns a value of undefined. * This is why setTimeout is used to delay the Promise function by using asynchronous logic
            setTimeout(() = > {
              //try... Catch can catch only synchronous exceptions, not asynchronous exceptions
                try {
                    const x = onResolved(this.value)
                    // Based on the type of x returned, determine the state of promise2 and call the corresponding state change function
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)}if (this.state === REJECTED) {
            setTimeout(() = > {
                try {
                    const x = onRejected(this.reason)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)}if (this.state === PENDING) {
            this.onResolvedCallbacks.push(() = > {
                setTimeout(() = > {
                    try {
                        const x = onResolved(this.value)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)})this.onRejectedCallbacks.push(() = > {
                setTimeout(() = > {
                    try {
                        const x = onRejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)})}})return promise2
}
Copy the code

The resolvePromise function is used to process the return value of the then parameter function. The return value type determines the state of the next promise object to be returned, thus determining which parameter function to execute the next THEN, according to the Promise/A+ specification:

  1. If promise2 and X point to the same object, the reject of promise2 is called and the exception is used as reason for promise2

  2. If the returned x value is a Promise, use its state

    If X is pending, promise2 must also remain pending until X becomes Resolved or Rejected

    If x is resolved, call the resolve method of promise2 and accept the current value as the first argument

    If x is rejected, call the reject method of promise2 and accept the current Reason as the first parameter

  3. If the x returned is a function or an object

    If the THEN property is a function, assume x is a promise object. If there is no THEN property, or the then property is not a function, then treat x as a normal value and call resolve(x) of promise2 directly.

  4. If the returned x is not a function or object, handle it as a normal value and call resolve(x) of promise2 directly.

function resolvePromise(promise2, x, resolve, reject) {
  Resolve /reject can only be called once in a promise
    let called;
    if (promise2 === x) {
        if (called) return;
        called = true
        reject(new TypeError("TypeError"))}if ((typeof x === 'object'&& x ! = =null) | |typeof x === 'function') {
        try {
            let then = x.then
            if (typeof then === 'function') {
              // x is a promise
                then.call(x, v= > {
                    if (called) return;
                    called = true
                    resolvePromise(promise2, v, resolve, reject)
                }, r= > {
                    if (called) return;
                    called = true
                    reject(r)
                })
            } else {
                if (called) return;
                called = true
                resolve(x)
            }
        } catch (e) {
            if (called) return;
            called = true
            reject(e)
        }
    } else {
        if (called) return;
        called = true
        resolve(x)
    }
}
Copy the code

The code is not very complicated, so I won’t explain it sentence by sentence. Here I will directly talk about the question I met at the beginning of learning

  1. Why does the then function throw an exception if the first argument of the then function is absent?

    First, the then function will receive two functions as arguments, and finally the two functions received by the THEN will only execute one of them. Which function will be executed depends on the internal state of the current Promise. When the internal state of the Promise is Resolved, the first function will be executed. The second function is executed when the Promise’s internal state is Rejected. Note also that when the executor function throws an exception during execution, the internal state also changes to Rejected.

    According to the Promise/A+ specification, if an exception E is thrown during the implementation of the promise1 ondepressing or onRejected, the state of promise2 will become Rejected and the onRejected implementing the promise2 object will be called

    So when the second argument is omitted, if you don’t throw an exception, whatever happens will make the state of the next Promise become Resolved and call the onResolve method that implements the next Promise object

  2. Why does some of the code in the then function need to be in the timer function?

    In order to get the promise2, the constructor of the promise2 must run out, so the synchronous code has to run first, generate the promise2 object, and then return to execute the asynchronous code in the constructor.

  3. Why use thene.call (x) in resolvePromise code instead of x.teng directly?

    // Let times = 1 object.defineProperty (x, 'then', getter() { if(times>1) { times++ return new Error() } return () => {} } )
    Copy the code

Promise to verify

There’s A validation tool online, written according to the Promise/A+ specification, that we can use to verify that our handwritten Promise code passes validation.

npm i promises-aplus-tests
Copy the code

After you download and install, you need to add a piece of execution code to your Promise file

Promise.defer = Promise.deferred = function () {    let dfd = {};    dfd.promise = new Promise((resolve,reject) = >{        dfd.resolve = resolve;        dfd.reject = reject;    })    returndfd; }Copy the code

Execute test command promises-aplus-tests promise.js:

reference

  1. Promise/A+
  2. Ruan Yifeng’s Promise object