Callbacks are the foundation of all asynchronous programming in Javascript, but using traditional callbacks to complete complex asynchronous processes is impossible to avoid the callback hell created by a large number of nested callbacks. To avoid the callback hell problem, the CommonJS community provides Promise’s specification with the goal of providing a more reasonable and powerful unified solution for asynchronous programming. It was later standardized into a language specification in ES2015.
A Promise is essentially an object that indicates whether an asynchronous task succeeds or fails when it finally ends. That is, the inner makes a promise to the outside world, which is initially a state Pending. This may be a big pity, but also may fail. There is a response to success or failure, and the corresponding task will be automatically executed after the commitment status is finally defined. The obvious feature of this commitment is that once the result is known, it is impossible to change and the corresponding state will perform the corresponding callback. For example, onFulfilled is successfully implemented and onRejected is failed.
Basic usage:
Promise at the code level is essentially a global type provided by Es2015 that we can use to construct a Promise instance, creating a new Promise. The constructor of this type needs to take a function as an argument that can be understood as a logic that does what it promises. This function is executed synchronously during Promise construction, and inside this function it accepts two arguments: resolve and reject, both of which are functions. The Resolve function changes the state of the Promise object to a big success pity. Usually, the result of the asynchronous task will be passed through this parameter. The Reject function changes the state of the Promise to Rejected, which passes an Error object, new Error(), to indicate why the Promise failed. Once the Promise instance is created, we can use the then method of the instance to specify the onFullfilled and onRejected callback functions, respectively. The first argument passed in to the then method is the ondepressing callback function, in which you can define some actions to perform upon success. The second argument passes the onRejected callback function, which internally defines some tasks to deal with if it fails.
const promise = new Promise(function(resolve, reject){
if (Math.random(1, 10) > 5) {
resolve(100)
} else {
reject(new Error('promise rejected'))
}
})
promise.then(
(value) => {console.log(value)}, // 100
(error) => {console.log(error)}, // promise rejected
)
Copy the code
Use cases:
Using Promise to encapsulate an Ajax, you define a function called Ajax that takes a URL argument that accepts the address of the request, and then within that function directly returns an instance of the Promise object, which essentially makes a Promise, Use the XMLHttpRequest object in the execution logic of the Promise object to send an Ajax request. Then you need to set the XHR request to the url in the parameters. Let’s go down and set the response type to JSON, which is a new feature introduced in HTML5. This way we can get a JSON object instead of a string after the request completes, and then register the XHR onLoad event. It’s also a new feature of HTML5 and this event is when the request completes, which is our traditional readyState of 4. During the request completion event, we should first determine whether the request status is 200, if it is, it means that the request has been successful. We should call Resolve to indicate that our Promise has been successful and pass the requested data back to Resolve. If the request fails, we call Reject to indicate that the Promise has failed, passing in an error message object that is the current status text. Call XHR’s send method to start executing the asynchronous request, and the Promise version of the Ajax function is wrapped.
function ajax (url) {
return new Promise(function(resovle, reject) {
const xhr = new XMLHttpRequest()
xhr.open('POST', url)
xhr.requestType = 'json'
xhr.onLoad = function () {
if (this.status === 200) {
resovle(this.response)
} else {
reject(new Error(this.statusText))
}
}
})
}
ajax('/api/users.json')
.then(
res => {},
error => {}
)
Copy the code
Common mistakes:
We’ve seen from the previous attempts that the essence of promises is to use callbacks to define the tasks that need to be performed after an asynchronous task ends, Only here the callback function is passed through the then method and Promise will classify our callback into successful ondepressing and failed onRejected. If there is a callback function, there will be callback hell. This leads us to the first myth of using promises, where chained calls should be made using the Promise object then method to keep asynchronous calls as flat as possible.
Chain call:
One big advantage of Promise is that it makes chained calls, thus minimizing callback hell. This is a big pity. The **then** method is to add a callback function for the **Promise object after the state is clear. The first parameter is onFulfilled, and the second callback is onRejected callback. The failed callback can be omitted, and the then method returns a Promise object internally. The promise that’s returned should be the same promise object as we thought we’d get from the chain call, it’s not really the same object. So instead of the usual chain call where you just return this inside a method, the then method returns a new Promise object, and the purpose is to implement a Promise chain where you return a new Promise after the Promise has been made. Each promise can be responsible for an asynchronous task, and they have no effect on each other, which means that if we continuously chain call then methods. Each then method here is actually adding a state-specific callback to the Promise object returned by the previous THEN method. These promises are executed sequentially, and the callbacks added here are naturally executed from front to back. Also, we can manually return a Promise object in the then callback, whose execution result will be called in the next THEN method, thus avoiding unnecessary callback nesting. And by analogy, if you have multiple consecutive tasks you can use the chain invocation to avoid nesting callbacks. To keep the code as flat as possible, if our callback returns not a Promise but a normal value, that value will be the Promise that the current THEN method returns, and the callback that the next THEN method receives will actually get that value, If it doesn’t return any value it returns undefined by default.
Conclusion: 1, the then method of the Promise object will return a new Promise object, so we can use the chain call method to add then method; The then method registers a callback for a Promise returned from the previous THEN method. The return value of the previous THEN method callback will be used as the parameter of the later THEN method callback. 4. If we return a Promise in a callback, then callbacks in the later THEN methods actually wait for the Promise to end, which means that the later THEN methods actually register the callback for the Promise we return.
Exception handling:
As mentioned earlier, if the Promise result fails, it calls the onRejected callback we passed in the then method. The onRejected callback will also be executed if an exception occurs or an exception is manually raised during the promise execution. The onRejected function does some processing for the exceptions in the Promise and is executed if the promise fails or an exception occurs. A more common way to register the onRejected callback is to use the Catch method of the Promise instance to register the onRejected callback. The catch method is an alias for the THEN method, because calling it actually calls the THEN method, and the first argument passes undefined. It is more common for a catch method to specify a failure callback because it is more suitable for chain calls. On the surface, we can register failed methods using catch method, which has the same effect as registering the second parameter of then method directly. Both of them can catch the exception during the execution of Promise. But if you look closely at the two methods, there’s a big difference, because each then method returns a new Promise object, which means that the catch that we call later in a chain call is actually specifying a failure callback to the Promise that the previous THEN method returns, Not directly specified for the first Promise object. Since this is the same Promise chain, exceptions on previous promises are passed back, so we can catch the exceptions in the first Promise, while the failure callback specified by the second argument to the then method is only specified for the first Promise object. That is, it can only catch exceptions for this Promise object. The apparent difference is that if we return a second Promise in the THEN method and an exception occurs during the execution of the Promise, we use the second argument to register the failure callback, which will not catch the exception of the second Promise. Because it is only a failed callback registered for the first Promise object, it is best to use the second method to specify success and failure callbacks separately in the case of chain calls. Because any exception in the Promise chain will be passed back until it is caught. That said, this approach is more like registering a failure callback for the entire Promise chain, so it’s relatively generic. In addition, we can register an onHandledrejection event on the global object to handle any Promise exceptions that are not caught manually in our code. The window object can be registered in the browser environment, and the Process object can be defined in the Node environment.
window.addEventListener('onhandledrejection', event => { const { reason, } = event event.preventDefault()}, false) process.addEventListener('onhandledRejection', (reason, promise) => { console.log(reason, promise) })Copy the code
Static method:
There are also two static methods within the Promise type that are often used. Resolve () will quickly convert a value into a Promise object that returns a successful result. Promise.resolve(‘foo’) will return a fulfilled Promise object. Foo is the return value of this Promise object, which means that the argument we can get in its onFulfilled callback is foo. This approach is exactly the same as the new Promise(resolve => resolve(‘foo’)) approach. If the method accepts another Promise, the Promise will be returned as is.
const promise = ajax('/api/user.json')
const promise2 = Promise.resolve(promise)
console,log(promise === promise2) // true
Copy the code
If we pass in an object and the object has the same THEN method as the Promise, and the then method can accept the onFulfilled and onRejected callback functions, then the object can also be implemented as a Promise object. Because this object also implements the thenable interface, in other words it is an object that can be then. The reason for supporting such objects is that in the days before native Promise objects became commonplace, promises were often implemented using third-party libraries.
Promise.resolve({
then(onFulfilled, onRejected) {
onFulfilled('foo')
}
}).then(value => {
console.log(value) // foo
})
Copy the code
In addition to the promise.resolve () method, there is a corresponding promisee.reject () method. What it does is quickly create a Promise that will result in a failure.
Promise.reject(new Error('rejected'))
.catch(error => console.log(error)) // rejected
Copy the code
Parallel execution:
All of the previous actions are cascading asynchronous tasks through promises, where one task is finished before the next one is started. Promise provides a flatter asynchronous programming experience than the traditional callback approach if multiple asynchronous tasks are executed in parallel. Promises can also provide a more complete experience, such as when multiple interfaces need to be requested on a page. If these interfaces are not dependent on each other, the best option is to request them at the same time to avoid the time consuming process of requesting them one by one. This kind of parallel request is actually easy to implement, we just need to call the Ajax function here separately. The hard part is figuring out when all the requests are over. Traditionally what we do is we define a counter and then every time we end a request, we let that counter accumulate. Until the number of this counter equals the number of tasks, all tasks are finished. This method can be very cumbersome, and there are exceptions to consider. In such cases it is much easier to use the all method of the Promise type, because this method can combine multiple promises into a single Promise to manage. Promise.all([….] ) receives an array, each of which is a Promise object. We can think of each of these promises as an asynchronous task, and this method will return a brand new promise object that will not be completed until all of the internal promises have been completed. In this case, the Promise object gets an array containing the results of each asynchronous task. The new Promise in this task will end only if all tasks are completed successfully. The Promise ends in failure if any of the tasks fail, which is a good way to execute multiple asynchronous Promise tasks synchronously.
// Use serial and parallel Ajax ('/ API /urls.json'). Then (value => {const urls = object.values (value) const tasks = urls.map(url => ajax(url)) return Promise.all(tasks) }).then(values => { console.log(values) })Copy the code
In addition to providing the promise.all () method, a promise.race () method is provided. This method can also combine multiple Promise objects into a brand new Promise object, but it differs from the promise.all () method. Promise.all() waits for all tasks to complete, whereas promise.race () follows the first completed task, meaning that the new Promise object returned will complete as soon as any one of the tasks completes.
const request = ajax('/api/users.json')
const timeOut = setTimeout(() => throw(new Error('Time Out')), 500)
Promise.race([
request,
timeOut
]).then(
value => console.log(value)
).catch (
error => console.log(error)
)
Copy the code
Execution sequence:
Finally, take a closer look at the issue of Promise execution timing, the order in which promises are executed. As you saw at the beginning, even though Promise doesn’t pass an asynchronous task internally, its callback function is still queued up in the callback queue. This means that we must wait for all current synchronization code to complete before we execute the Promise callback. If you continue to pass multiple callbacks in a chain-call fashion, each callback should be executed sequentially. However, if a Promise is preceded by a setTimeout, the Promise is executed first, thinking that setTimeout belongs to the macro task and Promise belongs to the micro task. Tasks in the callback queue are usually called macro tasks, and some additional requirements can be added during the execution of macro tasks. For these temporary additional requirements, you can choose to re-queue the callback as a new macro task. It can also be used as a microtask of the current task, which can be performed immediately after the current task has finished, rather than requeuing at the end of the queue. This is the difference between a macro task and a microtask. The Promise callback is executed as a microtask, so it will be automatically executed at the end of this call. The concept of microtask is actually introduced into JS later, its purpose is to improve the responsiveness of JS applications. Most of the asynchronous invocation apis we’ve been working with so far go into the callback queue as macro tasks, while the Promise object and process.nextTick in MutationObserver and Node are microtasks that are executed directly at the end of this call.
console.log('global start')
setTimeout(() => {
console.log('Time out')
}, 0)
Promise.resolve(10)
.then(value => {
console.log(value)
return ++value
})
.then(value => {
console.log(value)
return ++value
})
console.log('global end')
// global start
// global end
// 10
// 11
// Time out
Copy the code
Core logic analysis:
A Promise is a class that passes a function as an executor when instantiated and executes immediately. The executor takes two arguments, resolve and reject, which are essentially a function. Their purpose is to change the state of a Promise. There are three states in Promise: waiting pending, successful depressing and failed. Once the state is determined, it cannot be changed. This is a pity –> pity, pending –> pity, / –> pity. The resolve and reject functions are used to change the state. Resolve accepts a successful return value, reject accepts a failure cause.
Const PENDING = 'PENDING' // const REJECTED = 'REJECTED' // fail class Promise { constructor (executor) { executor(this.resolve, Resolve = () => {// If (this.status! This. Status = depressing} reject = () => {// This. Status = depressing} reject = () => {// This. == PENDING) return this.status = REJECTED}}Copy the code
What the then method does is determine the state, and if the state is successful then the success callback is called. Otherwise, the failed callback function is called. Then methods are defined on prototype objects. OnFulfilled (value) value indicates the value after success; onFulfilled(reason) Reason is the reason of failure.
class Promise { .... Value = undefined // Successful value Reason = undefined // failed cause resolve = value => {... This. Value = value} rejected = reason => {... This. Reason = reason // Save the value after failure} then (ondepressing, OnFulfilled (this. Value)} else if (this. Status === depressing) {// this === REJECTED) {// REJECTED (this.reason)}}Copy the code
The internal flow of asynchronous functions within a Promise is to determine the state of the Promise when the then function is called and executed, and to cache the successful and failed callbacks when the state is pending. Determine whether there is a cache of successful or failed callbacks in the resolve and reject bodies, and execute them.
class Promise { ... Ledcallback = undefined // Instance attribute successfully callback rejectedCallback = undefined // Instance attribute failed callback resolve = value => {.... This. Callback && this. Callback(value)} reject = reason => {... // Check whether the callback exists, This. Callback(reason)} then (onFulfilled, onRejected) {if (this. Status === depressing) {this. } else if (this.status === REJECTED) { ... This is a big pity. RejectedCallback = onrejectedCallback = onrejectedCallback = onFulfilled}}Copy the code
Then methods on the same Promise object can be called multiple times, which requires caching the asynchronous success or failure callback in an array, And to resolve or reject according to the sequence in the function body pop-up while (this) successCallback) length) enclosing successCallback. Shift () (value) this.
class Promise { ... None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None None While (enclosing fulfilledCallbacks. Length) / / whether the callback, There is invoked this. FulfilledCallbacks. Shift () (value)} reject = "reason = > {... While (enclosing fulfilledCallbacks. Length) / / whether the callback, There is invoked this. RejectedCallbacks. Shift () (reason)} then (onFulfilled onRejected) {if (this. The status = = = FULFILLED) {... } else if (this.status === REJECTED) { ... } else {/ / asynchronous deferred execution of wait states this. FulfilledCallbacks. Push (onFulfilled) enclosing rejectedCallbacks. Push (onRejected)}}}Copy the code
The THEN method can be called chained, and the subsequent THEN method callback gets the value returned by the THEN method callback. To implement a chained call to the THEN method, return a new Promise instance from the THEN method and pass the executor of the THEN into the new Promise. This is accomplished by passing the return value of the previous THEN callback to the next THEN by passing the return value of the execution of the successful callback to the new executor’s Resolve.
class Promise { ... then (onFulfilled, onRejected) { return new Promise((resolve, reject) => { if (this.status === FULFILLED) { const result = onFulfilled(this.value) resolve(result) } else if (this.status === REJECTED) { ... } else { ... }}}})Copy the code
If a Promise object is returned in the previous THEN callback, we need to type the return value when processing the THEN function.
class Promise {
...
then (onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
const result = onFulfilled(this.value)
resolvePromise(result, resolve, reject)
} else if (this.status === REJECTED) {
...
} else {
...
}
})
}
}
function resolvePromise (result, resolve, reject) {
if (result instanceof Promise) {
result.then(resolve, rejiect)
} else {
resolve(result)
}
}
Copy the code
If the Promise instance returns itself in the callback of then, the error of circular call will occur. To avoid this error, the chain call of THEN should be used to identify whether the Promise object is self-returned.
class Promise {
...
then (...) {
const promise = new Promise((...) => {
if (this.status === FULFILLED) {
setTimeout(() => {
....
resolve(promise, result, resolve, reject)
}, 0)
} else if (this.status === REJECTED) {
...
} else {
...
}
})
return promise
}
}
function resolvePromise (promise, result, resolve, reject) {
if (promise === result)
return reject(
new TypeError('Chaining cycle detected for promise #<Promise>')
)
...
}
Copy the code
Error capture takes into account two situations. The first is when an error occurs inside the executor, while the code in the executor is in the process of execution. When an error occurs, change the Promise state to a failed state. To catch this error in the second argument to the then method, add a try catch to the constructor outside the executor.
class Promise {
constructor (executor) {
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
...
}
Copy the code
The second is an error inside the then method’s callback, which is caught when the next THEN method executes. The implementation of this catch is to add a try catch outside of a successCallback or failCallback call in the then method body.
class Promise {
resovle = value => {
...
this.onFulfilledCallback.shift()()
}
reject = reason => {
...
this.onRejectedCallback.shift()()
}
then (onFulfilled, onRejected) {
const promise = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(_ => {
try {
const result = onFulfilled(this.value)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
} else if (this.status ==== REJECTED) {
setTimeout(_ => {
try {
const result = onRejected(this.reason)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
} else {
this.fulfilledCallbacks.push(_ => {
setTimeout(_ => {
try {
const result = onFulfilled(this.value)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
}) this.rejectedCallbacks.push(_ => {
setTimeout(_ => {
try {
const result = onRejected(this.reason)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
}) }
})
return promise
}
}
...
Copy the code
Make the argument to the THEN method optional: successCallback? successCallback : value => value failCallback ? failCallback : reason => throw reason