1. Introduction
In modern front-end development, front – end separation has become the mainstream. The back end provides restful interfaces, and the front end requests the data of the interface through Ajax, so that both parties have clear responsibilities and reduce their respective burdens.
This includes asynchronous network requests. We send an HTTP request on the front end, and after the interface returns the data, we can take it and do something about it.
Asynchronous requests are common in the front end and NodeJS. Therefore, how to gracefully handle asynchronous operations has been a challenge for front-end development.
2. The asynchronous
Having said that, what is asynchrony? Here’s a quote from Wikipedia:
Asynchronous conferencing is officially used in science to refer to computer-mediated technologies that communicate, collaborate and learn with some delay among interactive contributors. In contrast to synchronous communication, synchronous conferencing refers to various “chat” systems in which users communicate simultaneously “in real time”.
Programs that will be executed in the future can be considered asynchronous. If we register a setTimeout and do not execute the callback immediately, the behavior of setTimeout is asynchronous.
// Print first, third, second console.log('first'); setTimeout(() => { console.log('second'); }, 1000) console.log('third');Copy the code
If that doesn’t make sense to you, suppose we had a kettle. When we boil water, if we wait around until the water comes to the boil to get the water, that’s synchronization. If we go away and do our own thing while the water is boiling, it will remind you when the water is boiling, and you go to fetch the water, it is asynchronous. So what if we want to do something after the water boils? So that brings us to the two concepts we’ll cover in this section — callback functions and promises.
3. Callback functions
The first asynchronous processing we encountered was the callback function. We pass a callback to a function and specify that we wait for an operation to finish before executing the callback.
Const sleep = (time, callback) => {setTimeout(() => {callback(); }, time) }Copy the code
If you’ve worked with jQuery, you’ll be familiar with $. Ajax, which is typically used to retrieve asynchronous results via callback. We can execute the callback inside SUCCESS, or even pass data to the callback.
$.ajax({url: '/getBookList', method: 'GET', success(data) {// execute callback function}, fail(error) {// execute error callback function}})Copy the code
4. Disadvantages of callback functions
You may also think, doesn’t this look good for callback? It’s not elegant, but it’s clear. Why explore other ways?
4.1 Callback hell
For an example, suppose we control an animation of a traffic light switch (assuming the traffic light time is 60 seconds). Because each time depends on the previous completion, it can only be executed in the other party’s callback function. This creates a layer of nested callback functions.
green(60, function() { red(60, function() { green(60, function() { red(60, function() { green(60, function() { // ... })})})})})Copy the code
This code style is commonly referred to as callback hell. Intuitively, functions are nested on top of each other, making readability and maintainability very poor. When you want to modify the code inside the callback, you can only modify it inside the function, which also violates the open closed principle.
4.2 Error Tracing
At the same time, the presence of asynchrony causes the try… Catch cannot catch exceptions in asynchronous calls, making debugging difficult. In the following example, it is difficult to catch errors in asynchrony.
Const time = (callback) => {setTimeout(() => {try {console.log(aaaa) // AAAA undefined callback()} catch (err) {throw err } }, 1000) } const cb = () => { console.log('success') } // try... Try {time(cb)} Catch (err) {console.log('err', err)}Copy the code
There are two ways to solve this problem. One is to separate successful and failed callbacks, which jQuery does. We use the success and FAIL functions to handle both success and failure scenarios, passing caught exceptions to the Fail function.
function sleep(success, fail) {
setTimeout(() => {
try {
success();
} catch (err) {
fail(err);
}
}, 1000)
}
function success() {
console.log('success');
}
function fail(error) {
console.log('error: ', error);
}
sleep(success, fail);
Copy the code
The other is to return error as an argument. Many asynchronous interfaces in NodeJS do this.
readFile('test.txt', function(error, data) { if (error) { return error; } // failed // succeeded})Copy the code
4.3 Losing Control
In addition, since the function we depend on determines when and how many times the callback is executed, control is not on our side, which can lead to a lot of weird problems. Assuming we are using jQuery Ajax, what happens if jQuery executes our success method twice after the request interface succeeds? Of course, it’s hard to see such low-level problems with a project as well-maintained as jQuery, but it’s hard to guarantee that they won’t happen with other third-party libraries we use.
4.4 Parallel Problem
Suppose we have a scenario where we wait for all three interfaces to succeed before we perform an operation. How do we know when all three interfaces succeed? Do we set three different variables for each of the three interfaces, modify the value of the variable after successful execution, and determine in each interface?
let isAjaxASuccess = false, isAjaxBSuccess = false, isAjaxCSuccess = false; Function ajaxA (callback) {function ajaxasuccess = true; if (isAjaxBSuccess && isAjaxCSuccess) { callback(); }} function callback (callback) {callback = true; if (isAjaxCSuccess && isAjaxCSuccess) { callback(); }} function callback (callback) {callback = true; if (isAjaxCSuccess && isAjaxBSuccess) { callback(); }}Copy the code
Or do we set a setInterval for polling?
let isASuccess = false, isBSuccess = false, isCSuccess = false; Function ajaxA (callback) {callback = true; } function ajaxB (callback) {callback = true; } function ajaxC (callback) {isCSuccess = true; } const interval = setInterval(() => { if (isASuccess && isBSuccess && isCSuccess) { callback(); clearInterval(interval); }}, 500).Copy the code
Either way, I’m sure this code will drive you crazy. If an interface is added later, the scalability is also very poor.
5. Promise
And so, in ES2015, Promise was born. Promise successfully addressed issues such as nested calls and error tracing in callback functions, and control of callback functions.
If you haven’t used promises yet, you can read this article by Ruan Yifeng: ES6 Promise Objects
Promise is like a state machine, with three internal states: PENDING, REJECTED and depressing. Once you have transitioned from a PENDING state to another two states, you cannot transition to another state.
- If the state is PENDING, then the Promise can switch to the FULFILLED or REJECTED state.
- If this is a pity state, then the Promise cannot be changed into any other state.
- If it is the REJECTED state, then the Promise cannot be converted to any other state.
Promises provide then methods that can be invoked chained, allowing us to invoke then methods after the previous step (when the Promise moves from PENGDING to a depressing state).
const p = (value) => {
return new Promise((resolve, reject) => {
if (value > 99) {
resolve(value);
} else {
reject('err');
}
})
}
p(100).then(value => {
console.log(value); // 100
return value + 10;
}).then(value => {
console.log(value); // 110
return value + 10;
})
p(90).then(value => {
console.log(value);
return value + 10;
}).catch(err => {
console.log(err); // err
})
Copy the code
Note that the chained invocation of a Promise is not done by returning this in the then function as jQuery does. Instead, a new Promise object is returned each time, because the Promise state is irreversible. The value from each return in the previous THEN callback is passed in as an argument to the next THEN callback. If the return is a Promise object, then the then method waits for the Promise to complete before executing the callback. Guess what happens to the following program?
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
}, time)
})
}
const promise1 = sleep(100);
promise1.then(function onFulfilled1(value) {
const promise2 = sleep(1000);
return promise2;
})
.then(function onFulfilled2(value) {
console.log("success");
})
Copy the code
Promise1 will become the depressing state after 100ms, and the onfunction 1 will be invoked. In Onhelix 1, we end up returning promise2 again. So when is onled2 going to execute here? Is it executed immediately after the ononled1 execution? Of course not. This is also because the implementation of the THEN method creates a new promise each time. In fact, the second THEN is invoked by this new promise. This promise will wait until the promise2 state returned by the current THEN method becomes a big pity before invoking the callback in the next THEN.
Note: When you pass a value instead of a function to then, that value is passed transparently to the next THEN.
new Promise((resolve) => {
resolve(111)
})
.then(2222)
.then(v => {
console.log(v) // 111
})
Copy the code
5.1 Promise handles asynchrony
We used Promise to rewrite the traffic light code above:
const greenAsync = (time) => {
return new Promise((resolve) => {
green(time, function() {
resolve()
})
})
}
const redAsync = (time) => {
return new Promise((resolve) => {
red(time, function() {
resolve()
})
})
}
greenAsync(60).then(() => {
return redAsync(60)
}).then(() => {
return greenAsync(60)
}).then(() => {
return redAsync(60)
})
Copy the code
As you can see, the promise-wrapped code separates the code implementation from the callback function, perfectly addressing the high coupling that comes with function nesting. With Promise, we also gained control over the callback function, and we could dictate when and how many times it should be executed, which solved the trust problem perfectly. Promise also addresses the difficulty of debugging errors caused by function nesting. Promise provides catch methods that can be used to catch exceptions thrown when the callback function executes. Let’s further modify the time function above.
const cb = () => { console.log('success') } const time = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { try { console.log(aaaa); Resolve ()} catch (err) {reject(err)}}, time * 1000) }) } time(60).then(cb).catch(err => { console.log('err', err) })Copy the code
5.2 Promise. All and Promise. Race
As for waiting for multiple requests to succeed before performing an operation, Promise also provides a static method called ALL, which takes an array of Promises and returns an array of values in its then callback function. Obviously, this array is the set of values returned from the previous Promise array. The Promise performs the callbacks in the THEN after all promises have become a pity state, but immediately calls the catch method when any Promise becomes a REJECT state.
Promise.all([ajaxA, ajaxB, ajaxC]).then(dataArr => {// where dataArr is the set of results returned by each request})Copy the code
In addition to promise.all, a static method of promise.race is provided. This method is executed the exact opposite of promise.all. This is a big pity. Race means to race. As long as one of the Promise states becomes a big pity, it will immediately perform the callback of then.
Promise.race([ajaxA, ajaxB, ajaxC]).then(value => {// which return fast, value is which})Copy the code
Principle 6. Promise
Promise isn’t some arcane API, but we’ve analyzed a lie.js library based on the PromiseA+ standard to understand how it works.
6.1 Promise class
First of all, from the way it is called. Promise objects are usually created using the new operator, which tells you that a Promise must be a class. Then, the class can have three states, so there must be a state property to hold the current state. Meanwhile, the Promise constructor accepts a function and passes resolve and reject as arguments. In addition, we need to save the value returned or the exception thrown after the current operation is executed, so that we can pass it to the then or catch callback.
const REJECTED = 'REJECTED';
const FULFILLED = 'FULFILLED';
const PENDING = 'PENDING';
class Promise {
static all() {}
static race() {}
constructor(resolver) {
this.state = PENDING;
this.outcome = void 0;
this.queue = [];
safelyResolveThenable(this, resolver);
}
then() {}
catch() {}
}
Copy the code
Next, let’s deal with the constructor. In the constructor, we call resolve or Reject to change the state of the Promise. This step is implemented in lie like this. Handlers. OnResolve will be analyzed later.
function safelyResolveThenable(promise, resolver) { let isCalled = false; Function onError(error) {if (isCalled) return; called = true; handlers.onError(promise, error); } function onResolve(value) { if (isCalled) return; called = true; handlers.onResolve(promise, value); } try { thenable(onSuccess, onError); } catch (err) { onError(err); }}Copy the code
6.2 Delayed Execution
If we set setTimeout in the constructor to resolve after the Promise’s then method has already been executed, how can we guarantee that the then callback will be executed after 1000ms?
new Promise((resolve) => {
setTimeout(() => {
resolve(1111)
}, 1000)
}).then(value => {
console.log(value); // 1111
})
Copy the code
Does this delay remind you of publish-subscribe, which you use a lot? We put the function in an array and wait until the time is right to execute it. So we can use a queue to hold registered THEN callbacks that will be executed after setTimeout. It can be easily understood with the following code:
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
var p = promise.then(function() {})
var q = promise.catch(function() {})
console.dir(promise)
Copy the code
Printed promises should look like this (although you won’t see this if you use ES6 promises).
Promise {
state: 0,
outcome: undefined,
queue:
[ QueueItem {
promise: Promise { state: 0, outcome: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] },
QueueItem {
promise: Promise { state: 0, outcome: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] } ] }
Copy the code
P is promise.queue[0]. Promise.
6.3 then
So how do you implement the registration callback function? In the then method, we can determine whether the current Promise state is PENDING, and if so, we can place the callback function in the current Promise queue.
// This will be a big pity. // This will be a big pity. // This will be a big pity. The value of the current Promise will be transparent to the next then if (typeof ondepressing! == 'function' && this.state === FULFILLED || typeof onRejected ! == 'function' && this.state === REJECTED) { return this; } // create a new promise object var promise = new this.constructor(INTERNAL); // If the current state is PENDING, it needs to be queued, otherwise it is executed directly. if (this.state ! == PENDING) { var resolver = this.state === FULFILLED ? onFulfilled : onRejected; Unwrap (Promise, resolver, this.outcome); } else { this.queue.push(new QueueItem(promise, onFulfilled, onRejected)); } return promise; };Copy the code
As you can see from above, if the current state is no longer PENDING, the result of the execution is passed to the unwrap method along with the callback function. The unwrap method is as follows:
function unwrap(promise, func, value) { immediate(function () { var returnValue; try { returnValue = func(value); } catch (e) { return handlers.reject(promise, e); } if (returnValue === promise) { handlers.reject(promise, new TypeError('Cannot resolve promise with itself')); } else { handlers.resolve(promise, returnValue); }}); }Copy the code
The unwrap method is simple. It simply executes the callback passed to then/catch and passes the result to handlers.resolve. There is also a library called immediate, which is actually an asynchronous method, which is why the then method is asynchronous. Handlers. Resolve is also used in safelyResolveThenable.
handlers.resolve = function (self, value) { var result = tryCatch(getThen, value); if (result.status === 'error') { return handlers.reject(self, result.value); } var thenable = result.value; SafelyResolveThenable if (thenable) {safelyResolveThenable(self, thenable); safelyResolveThenable if (thenable) {safelyResolveThenable(self, thenable); } else { self.state = FULFILLED; self.outcome = value; var i = -1; var len = self.queue.length; while (++i < len) { self.queue[i].callFulfilled(value); } } return self; };Copy the code
TryCatch (getThen, value) is the then method above the value, equivalent to result.value = value.then, so it is used to determine whether the incoming value is a Promise object. As you can see, if thenable exists, the safelyResolveThenable method will be called again if the current pass is still a Promise. As mentioned earlier, a new Promise is created in then, and the state of the Promise is changed based on the Promise returned by the then function. So calling safelyResolveThenable again is to change the state of self based on the result of thenable. If that’s confusing to you, let me show you a little bit of code.
const sleep = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(111)
}, 1000);
})
}
new Promise(r => r(222)).then(value => {
return sleep();
})
Copy the code
In this code, a total of three promises are involved, namely, new Promise, Promise created in then and Promise returned by sleep. When the THEN function is executed, a new Promise2 is created. After 1000ms, the first Promise1 state becomes a pity, the callback function in THEN is called, and a new Promise3 is returned in sleep. This is a big pity when the Promise2 becomes a big pity after the Promise3 becomes a big pity.
6.4 the queue
If thenable is not present, then either resolve receives or THEN returns a Promise object. You can see that the state is FULFILLED, and the callFulfilled method in the queue mounted on the current Promise object is iterated and executed. So how is a queue implemented? From the above we can see that there is a calldepressing method.
function QueueItem(promise, onFulfilled, onRejected) { this.promise = promise; // If (typeof onFulfilled === 'function') {this. OnFulfilled = onFulfilled; this.callFulfilled = this.otherCallFulfilled; If (typeof onRejected === 'function') {this.onRejected = onRejected;} // If (typeof onRejected === 'function') {this.onRejected = onRejected; this.callRejected = this.otherCallRejected; } } QueueItem.prototype.callFulfilled = function (value) { handlers.resolve(this.promise, value); }; QueueItem.prototype.otherCallFulfilled = function (value) { unwrap(this.promise, this.onFulfilled, value); }; QueueItem.prototype.callRejected = function (value) { handlers.reject(this.promise, value); }; QueueItem.prototype.otherCallRejected = function (value) { unwrap(this.promise, this.onRejected, value); };Copy the code
When we store the then callback function in the queue, the callFulfilled method is actually otherCallFulfilled, and the otherCallFulfilled method is still the unwrap method called. Given the implementation of unwrap, it is obvious that the state of this.promise is changed and value is attached to it, which is why the then callback waits for the promise state to change before executing.
Such a basic Promise implementation principle is very clear, in fact, it is mainly three state transitions, with queue to realize the delayed implementation of THEN.
7. To summarize
This article introduces two of the most common methods of asynchronous JS programming: callback functions and promises. Promise can be used to make code more readable by wrapping cumbersome callback functions more succinct. Promise is not the ultimate solution to asynchrony, of course, and has its drawbacks. In the next article I will introduce two other solutions, Generator and await.
Recommended reading
- In-depth Promise(a) — Promise implementation details
- Deep into promises (II) – Advancing promises
- Deep into promises (iii) — Name promises