This article will introduce the principles of Promise through A combination of diagrams and source code, focusing on Promises/A+ specifications to help you better understand Promises.

The source code mentioned below is the unofficial source code, but the mock source code is mainly used because it is easier to understand than the official source code. The source code runs through Promises/A+ all test cases, so the functionality is the same.

The essence of the Promise

A Promise represents a Promise that an asynchronous operation does not yet have a result, but promises to inform the user of the result at some point in the future.

let promise1 = new Promise((resolve, reject) = > {
    try {
        // Some asynchronous operations, such as setTimeout, or asynchronous requests
        setTimeout(() = > {
            resolve(1)},1000)
    } (err) {
        // Tell the error message when an error occurs
        reject(err)
    }
})
Copy the code

Graphically, each promise instance has two internal interfaces, resolve and reject, which are exposed to the outside in the form of callback function parameters, transferring the call authority to the outside, which is equivalent to telling the user: “Hey buddy, please feel free to carry out your task. I am here all the time. Let me know when you have a result.

Promise objects have three states: Pending, depressing, and Rejected.

pending – The initial state of a promise.

fulfilled – The state of a promise representing a successful operation.

rejected – The state of a promise representing a failed operation.

This process can only be fulfilled by pending or fulfilled. Moreover, the state is irreversible. We define pending as “uncompleted” state, and fulfilled and rejected as “completed” state.

Let’s take a look at the source of the Promise constructor:

function Promise(f) {
  this.result = null
  this.state = PENDING
  ...
  let ignore = false

  let resolve = value= > {
    if (ignore) return
    ignore = true
    resolvePromise(this, value, onFulfilled, onRejected)
  }

  let reject = reason= > {
    if (ignore) return
    ignore = true
    onRejected(reason)
  }

  try {
    f(resolve, reject)
  } catch (error) {
    reject(error)
  }
}
Copy the code

The resolve and reject functions are local functions within the constructor that are passed as two arguments to the f executor. There is also an ignore local variable that ensures that the logic in resolve and Reject is executed only once. In addition, the state attribute represents the current state of the promise, while result represents the end result of the promise. This is a big pity. I will surely forget my success and my rejected represents the reason for failure.

Then method

Then is the key method for promises, and the user uses it to get asynchronous results. It takes two function parameters to get a success result and a failure reason.

promise1.then((data) = > {
    console.log('Success:', data)
}, (err) = > {
    console.error('Failure:', err)
})
Copy the code

According to the Promise specification, the then method returns a new Promise instance, which we assume is promise2. Promise1 references the resolve and reject of promise2, which is the key to the promise chain invocation, as we’ll see later.

Then method:

Promise.prototype.then = function(onFulfilled, onRejected) {
  return new Promise((resolve, reject) = > {
    let callback = { onFulfilled, onRejected, resolve, reject }

    if (this.state === PENDING) {
      this.callbacks.push(callback)
    } else {
      setTimeout(() = > handleCallback(callback, this.state, this.result), 0)}}}Copy the code

The code is very simple, with a callback object to save the ondepressing, onRejected functions of the current promise and the resolve and reject functions of the next promise. If the promise state is not finished, it is temporarily stored in the Callbacks array; If the state is completed, the callback is immediately processed with the result of the completed state.

The diagram is as follows:

Chain calls

As mentioned above, the then method returns a new promise object, so the later THEN method is the method of the second promise, and so on, forming a promise chain, as illustrated below:

Sort of like a one-way data list with no:

Unidirectional data linked lists

When promise1’s resolve is called, the onFulfilled function will be judged and called first, and when Promise1’s Reject is called, the onFulfilled function will be judged and called.

After the onFulfilled or onRejected logic is processed, the processing result is transferred to the resolve or reject of promise2. Thus, the state of promise2 will be combined and changed, which is the essence of promise chain call.

Promise synchronization task

Promise is typically used for asynchronous tasks, but synchronous tasks are also supported, so guess what happens to the following code:

new Promise((resolve, reject) = > {
    console.log(1)
    resolve(1)
})
.then((result) = > {
    console.log(2)})console.log(3)

// Output the result
1
3
2
Copy the code

It can be seen that even if the synchronous resolve is fulfilled, the ondepressing function cannot be executed synchronously, because setTimeout is used to ensure the asynchronous execution of ondepressing. On the source code:

Promise.prototype.then = function(onFulfilled, onRejected) {
  return new Promise((resolve, reject) = > {
    let callback = { onFulfilled, onRejected, resolve, reject }

    if (this.state === PENDING) {
      this.callbacks.push(callback)
    } else {
      Use setTimeout to ensure asynchrony
      setTimeout(() = > handleCallback(callback, this.state, this.result), 0)}}}const transition = (promise, state, result) = > {
  if(promise.state ! == PENDING)return
  promise.state = state
  promise.result = result

  SetTimeout is also used to guarantee asynchrony when state changes
  setTimeout(() = > handleCallbacks(promise.callbacks, state, result), 0)}Copy the code

Resolve promise instance

This is a big pity. OnFulfilled function can return any valid JS value, including undefined, promise instance and Thenable object. Here I will focus on the promise instance. The test code is as follows:

let promise1 = new Promise((resolve, reject) = > {
    try {
        // Some asynchronous operations, such as setTimeout, or asynchronous requests
        setTimeout(() = > {
            resolve(1)},1000)
    } (err) {
        // Tell the error message when an error occurs
        reject(err)
    }
})

let externalPromise = new Promise((resolve, reject) = > {
    setTimeout(() = > {
        resolve('task result')},1000)
})

promise1.then((data) = > {
    return externalPromise   
}).then((data) = > {
    console.log('Success:', data)
})
Copy the code

Promise1. Then generates promisE2, and the second then generates promise3. The ondepressing of promise1 returns the externalPromise object. When externalPromise fulfill is fulfilled, the resolve function of promise2 will be called, thus triggering the resolve process of promise2. The ondepressing function of promise2 is invoked, and the resolve link is restored.

It’s kind of like the marital situation had a little hiccup and finally got back on track.

Resolve themselves

Promises/A+ do not allow resolve Promises on their own. Why is that?

First look at the source code:

function Promise(f) {...let ignore = false
  let resolve = value= > {
    if (ignore) return

    ignore = true
    resolvePromise(this, value, onFulfilled, onRejected)
  }
  ...

  try {
    f(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

const resolvePromise = (promise, result, resolve, reject) = >{...if (isPromise(result)) {
    return result.then(resolve, reject)
  }
  ...

  resolve(result)
}
Copy the code

This is a big pity. When the value of resolve is itself, the promise of resolvePromise function === result, the onFulfilled promise and the then method of onRejected to result will be waited for the result. This leads to an endless cycle of waiting for yourself. Test it out:

let promise = new Promise((resolve, reject) = > {
    setTimeout(() = > {
        resolve(promise)
    }, 0)
})

promise.then((result) = > {
    console.log('resolve:', result)
})

// Output the result

VM783:3 Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
    at <anonymous>
    at <anonymous>:3:9
Copy the code

The “weird” Reject process

In the promise chain, the flow of Reject is different from that of Resolve, which I didn’t understand when I first encountered Promise. Reject process is shown as follows: when the header promise rejected is rejected, all subsequent promises are reject until the first promise with onRejected is met. The implementation result of onRejected is used, and the promise after resolve is used. To put it bluntly, the state “reverses.”

new Promise((resolve, reject) = > {
  setTimeout(() = > {
    reject(1)},10)
})
.then((result) = > {
  console.log('onResolved_1', result)
  return 'onResolved_1'
}, (reason) = > {
  console.log('onRejected_1', reason)
  return 'onRejected_1'
})
.then((result) = > {
  console.log('onResolved_2', result)
  return 'onResolved_2'
}, (reason) = > {
  console.log('onRejected_2', reason)
  return 'onRejected_2'
})
.then((result) = > {
  console.log('onResolved_3', result)
  return 'onResolved_3'
}, (reason) = > {
  console.log('onRejected_3', reason)
  return 'onRejected_3'
})
.catch(() = > {
  console.log('catch', reason)
})

// Output the result
onRejected_1 1
onResolved_2 onRejected_1
onResolved_3 onResolved_2
Copy the code

This is a big pity. We can see that after the onRejected of the first THEN, the onFulfilled function of the second THEN will be triggered, but we will receive the result of the onRejected of the first THEN. Or look at the source code image:

const handleCallback = (callback, state, result) = > {
  let { onFulfilled, onRejected, resolve, reject } = callback
  try {
    if (state === FULFILLED) {
      isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
    } else if (state === REJECTED) {
      isFunction(onRejected) ? resolve(onRejected(result)) : reject(result)
    }
  } catch (error) {
    reject(error)
  }
}
Copy the code

This is a big pity. We can see that when state === depressing, if onRejected is the function, the resolve interface of the next promise will be called after onRejected is implemented.

This is designed to give the user a chance to handle errors without affecting subsequent chain calls. Note that the onResolved parameter in the second THEN needs to determine the type of result, otherwise the code is prone to errors.

Why was my last catch callback not executed?

The essence of catch is the then function which ondepressing function is null, refer to the official implementation.

// https://github.com/then/promise/blob/master/src/es6-extensions.js
Promise.prototype['catch'] = function (onRejected) {
  return this.then(null, onRejected);
};
Copy the code

The promise state is reversed and the resolve state is changed to resolve, so the onRejected promise will not be called.

Exception handling in chain calls

Please state the result of the following code:

new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve(1)},10)
})
.then((result) = > {
  console.log('onResolved_1', result)
  throw new Error('custom error')},(reason) = > {
  console.log('onRejected_1', reason)
  return 'onRejected_1'
})
.then((result) = > {
  console.log('onResolved_2', result)
  return 'onResolved_2'
}, (reason) = > {
  console.log('onRejected_2', reason)
  return 'onRejected_2'
})

// Run the result
onResolved_1 1
VM713:17 onRejected_2 Error: custom error
Copy the code

Most of you should have guessed the answer, but why? A look at the source code may be more understandable:

const handleCallback = (callback, state, result) = > {
  let { onFulfilled, onRejected, resolve, reject } = callback
  try {
    if (state === FULFILLED) {
      isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
    } else if (state === REJECTED) {
      isFunction(onRejected) ? resolve(onRejected(result)) : reject(result)
    }
  } catch (error) {
    reject(error)
  }
}
Copy the code

Ondepressing and onRejected are the two parameters of the first THEN function, and resolve and reject are the resolve and reject interfaces of the first THEN function. As we can see, there is a layer of try in the outer part of the promise object. Catch: If the internal code fails, the next Promise’s Reject interface is called instead of the onRejected function of the current promise.

Understand the nature of catch and exception handling in chain calls, and give my own best practices based on my years of experience:

  • The onRejected function of the then method is left empty, and the last catch method handles the exception
  • The then method is no longer added after catch
let syncTask = (resolve, reject) = > {}
let handle1 = (res) = >{... }let handle2 = (res) = >{... }let handle3 = (res) = > {...}
...

let onError = (err) = > { console.log('onError:', err) }

let p = new Promsie(syncTask)
p
.then(handle1)
.then(handle2)
.then(handle3)
.catch(onError)
Copy the code

The realization of the Promise. Resolve

We often see the following code:

Promise.resolve(1)
.then((result) = > {
    console.log('result:', result)
})
Copy the code

Promise. Resolve is not in Promises/A+, but in Promises/A+, it is very easy to implement A Promise.

Promise.resolve = function (value) {
    if (value instanceof Promise) return value;
    return new Promise((resolve, reject) = > {
        resolve(value)
    })
};
Copy the code

See the official implementation for details: github.com/then/promis…

The last

The above is my understanding of Promise. I hope it can help students who are learning Promise to better understand. Finally, attach a set of Promise quizzes and see if you can answer them all correctly.

I Promise I will (10 questions)

Reference documentation

  1. Promises/A + specification
  2. Promises/A+ 100 lines of code
  3. I Promise I will (10 questions)
  4. Promise won’t…? Look here!! The most accessible Promise ever!!
  5. The official source