This is the fourth article on how JavaScript works. The other three can be found here:
- How JavaScript works: An overview of the engine, runtime, and call stack
- How JavaScript works: Inside the V8 Engine + 5 tips on how to write optimized code
- How JavaScript works: Memory management and four common memory leaks
This time we’ll expand on our first article by reviewing the drawbacks of programming in a single-threaded environment and how to overcome them to build amazing JavaScript UIs. In keeping with tradition, at the end of this article we will share five tips on how to write cleaner code using async/await.
Why are single threads limited?
In the first article, we addressed the issue of what happens when the call stack contains a function call that takes a long time to run. Imagine, for example, a complex image conversion algorithm running in a browser. At this point, there are functions running on the stack, and the browser can’t do anything. At this point, he is blocked. That means it can’t render, it can’t run other code, it’s stuck, there’s no response. This brings up the problem that your program is no longer efficient. Your program doesn’t work anymore. In some cases, this is no big deal, but it can cause more serious problems. If the browser is running too many tasks on the call stack at the same time, the browser will stop responding for a long time. At that point, most browsers throw an error asking whether to terminate the page.
It’s ugly and it completely destroys the user experience of the application.
Building blocks for Javascript programs
You might write JavaScript programs in a single.js file, but programs are made up of multiple code blocks. Currently, only one code block is running, and the others will run later. The most common block unit is a function. The problem many new JavaScript developers may need to understand is that running after means that it does not have to be executed immediately after now. In other words, tasks that, by definition, cannot be completed now will be completed asynchronously so that you don’t inadvertently encounter the UI blocking mentioned above. Look at the following code:
// Ajax provides any Ajax function for a library
var response = ajax('https://example.com/api');
console.log(response);
// 'response' will have no data returned
Copy the code
You probably already know that standard Ajax requests don’t execute completely synchronously, meaning that ajax(..) The function does not return any value to the response variable
An easy way to get the return value of an asynchronous function is to use a callback function.
ajax('https://example.com/api'.function(response) {
console.log(response); // 'response' now has a value
});
Copy the code
Just be careful: never make synchronous Ajax requests, even if you can. If a synchronous Ajax request is made, the JavaScript application’s UI will be blocked – the user cannot click, enter data, jump, or scroll. Any user interaction will be blocked. This is very bad.
The following example code, but please don’t do this, it will ruin the page:
// Assuming you use jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint'.success: function(response) {
// Successful callback.
},
async: false / / synchronize
});
Copy the code
Take an Ajax request as an example. You can execute arbitrary code asynchronously.
You can use the setTimeout(callback, milliseconds) function to execute code asynchronously. The setTimeout function fires the event (timer) at some later time. The following code:
function first() {
console.log('first');
}
function second() {
console.log('second');
}
function third() {
console.log('third');
}
first();
setTimeout(second, 1000); // Call second after 1 second
third();
Copy the code
The console output is as follows:
first
third
second
Copy the code
Profiling the event cycle
We start with a strange statement here — although asynchronous JavaScript code is allowed (as discussed in the example above with setTimeout), JavaScript itself actually never had any notion of built-in async before ES6, and the JavaScript engine only executes one block at any given time.
For more on how JavaScript engines work, see the first article in this series
So, who tells the JS engine to execute the code block of the program? In fact, the JS engine doesn’t run on its own — it runs in a host environment, which for most developers is a typical Web browser or Node.js. In fact, JavaScript is now embedded in a variety of devices, from robots to lightbulbs, each representing a different type of hosting environment for the JS engine.
Common in all environments is a built-in mechanism called an event loop, which handles the execution of multiple blocks of the program each time the JS engine is called.
This means that the JS engine is just an on-demand execution environment for arbitrary JS code, and the host environment handles the event execution and results.
For example, when a JavaScript program makes an Ajax request to fetch some data from the server, setting the “response” code in the function (” callback “), the JS engine tells the host environment :” I’m going to defer execution for now, but when I complete that network request, I’ll return some data, Please call back this function and pass the data to it.
The browser then listens for the response from the network, and when it listens for the network request to return content, the browser schedules the callback function to execute by inserting it into the event loop. Here is a schematic diagram:
You can read more about the memory heap and call stack in our previous article.
What are these Web apis? In essence, they are inaccessible threads that can only be called. They are the concurrent part of the browser. If you’re a node.js developer, these are c++ apis.
So what exactly is the cycle of events?
The event loop has a simple job — monitoring the call stack and callback queue. If the call stack is empty, it gets the first event from the queue and pushes it to the call stack, which runs it effectively.
Such iterations are called in the event loop (tick), and each event is just a function callback.
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
Copy the code
Let’s execute this code to see what happens:
1. The initial states are empty, the browser console panel is empty, and the call stack is empty.
2.console.log('Hi')
Is added to the call stack.
3.console.log(Hi)
Be implemented.
4.console.log('Hi')
Remove from the call stack.
5.setTimeout(function cb1() { ... })
Is added to the call stack
6.setTimeout(function cb1() { ... })
When executed, the browser creates a timer through its Web APIS to time your code.
7. ThesetTimeout(function cb1() { ... })
The call timer itself is a function that has been completed and removed from the call stack.
8.console.log('Bye')
Is added to the call stack.
9.console.log('Bye')
Be implemented.
10.console.log('Bye')
Remove from the call stack.
11. After the timer is executed at least 5000ms, run thecb1
The callback function is added to the callback queue.
12. The event loopcb1
Taken from the callback queue and added to the call stack.
13.cb1
To be carried outconsole.log('cb1')
Add to the call stack.
14.console.log('cb1')
Be implemented.
15.console.log('cb1')
Remove from the call stack.
16.cb1
Remove from the call stack.
Overall Process review:
setTimeout(…) How it works
Note that setTimeout(…) Callbacks are not automatically placed in the event loop queue. It sets a timer. When the timer expires, the environment puts the callback into the callback so that it will be received and executed by a future tick. Take a look at the following code:
setTimeout(myCallback, 1000);
Copy the code
This does not mean that myCallback will be executed after 1000ms, but after 1000ms, myCallback will be added to the callback queue, which may also have other events added earlier, in which case your callback will have to wait.
Many articles and tutorials recommend using setTimeout(callback,0) when starting asynchronous programming in JavaScript. Now that you know how the event loop works and how setTimeout works, Calling setTimeout 0 ms as the second argument simply delays the callback and puts it on the callback queue until the call stack is empty.
Take a look at the following code:
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
Copy the code
Although the wait time is set to 0ms, the browser prints the following:
Hi
Bye
callback
Copy the code
What is a task queue in ES6?
In the INTRODUCTION of ES6, there is a new concept called “task queue”, which is a layer on top of the event loop queue, most commonly when promises are handled asynchronously. Let’s discuss this concept now so that we can understand how Promises are scheduled and handled when we talk about asynchronous behavior with Promises.
Imagine this: The task queue each tag is attached to the event loop the queue (from a callback after the team to get data, on the call stack implementation process) at the end of the queue, some asynchronous operations may occur during the event loop of a tag, will not lead to a whole new events are added to the circular queue, Instead, an item (that is, a task) is added to the end of the task queue for the current tag.
This means it’s safe to add another function for later execution, which will be executed immediately before anything else.
A task may also create more tasks to add to the end of the same queue. In theory, a task “loop” (adding any other tasks, etc.) could run indefinitely, depriving the program of the resources necessary to move on to the next event loop marker. Conceptually, this is similar to expressing long running or infinite loops in code (such as while (true)..) .
The task is a bit like a setTimeout(callback, 0) “hack,” but implemented by introducing a more well-defined and guaranteed order: execute later, but as quickly as possible.
The callback
As you already know, callbacks are by far the most common way to express and manage asynchrony in JavaScript programs. In fact, callbacks are the most basic asynchronous pattern in the JavaScript language. Countless JS programs, even very complex programs, except some are basically written on the basis of callback asynchrony. However, callback functions still have some drawbacks, and developers are trying to explore better asynchronous modes. However, it is impossible to use any abstracted asynchronous pattern effectively without understanding the underlying processes. In the next chapter, we’ll delve into these abstractions to show why more complex asynchronous patterns (to be discussed in a subsequent article) are necessary and even recommended.
Nested callbacks
Take a look at the following code:
listen('click'.function (e){
setTimeout(function(){
ajax('https://api.example.com/endpoint'.function (text){
if (text == "hello") {
doSomething();
}
else if (text == "world") { doSomethingElse(); }}); },500);
});
Copy the code
We form a nested chain of three functions, each representing a step in the asynchronous series. This code is often referred to as “callback hell.” But “callback hell” actually has almost nothing to do with nesting/indentation, which is a deeper problem. First, we wait for the “click” event, then for the timer to fire, then for the Ajax response to return, at which point we might repeat everything again. At first glance, this code seems to correspond its asynchronous process to the following steps in which multiple functions execute sequentially:
listen('click'.function (e) {
// ..
});
Copy the code
And then:
setTimeout(function(){
// ..
}, 500);
Copy the code
And then:
ajax('https://api.example.com/endpoint'.function (text){
// ..
});
Copy the code
Finally:
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
Copy the code
So is it more natural to express your asynchronous nested code in a synchronous way? There has to be a way, right?
Promise
Take a look at the following code:
var x = 1;
var y = 2;
console.log(x + y);
Copy the code
Pretty straightforward, the sum of x and y is printed in console.log. What if the values of x and y have not been assigned yet, and I still need to find the values? For example, you need to fetch the values of X and y from the server before you can use them in an expression. Suppose we have a function loadX and loadY that load the values of X and YY from the server, respectively. Then, once x and y are both loaded, we have a function sum that sums the values of x and y. It might look something like this:
function sum(getX, getY, callback) {
var x, y;
getX(function(result) {
x = result;
if(y ! = =undefined) { callback(x + y); }}); getY(function(result) {
y = result;
if(x ! = =undefined) { callback(x + y); }}); }// A sync or async function that retrieves the value of `x`
// Get the x value method
function fetchX() {
// ..
}
// A sync or async function that retrieves the value of `y`
// get the y value method
function fetchY() {
// ..
}
/ / call
sum(fetchX, fetchY, function(result) {
console.log(result);
});
Copy the code
There is something very important in this code, we take x and y as values taken asynchronously, and we execute a function sum(…) (externally), it doesn’t care about X or Y, or whether they are immediately available.
Of course, this crude callback-based approach leaves a lot to be desired. This is just a small step where we don’t have to decide what to do with the value of an asynchronous request.
Promise Value
Let’s take a quick look at how we use promise for x+y:
function sum(xPromise, yPromise) {
// `Promise.all([ .. ] )` takes an array of promises,
// and returns a new promise that waits on them
// all to finish
//`Promise.all([ .. ] ) 'pass in a Promise array,
// By returning a new promise, the promise will wait for all returns
return Promise.all([xPromise, yPromise])
// when that promise is resolved, let's take the
// received `X` and `Y` values and add them together.
// When the promise is resolved, return the x and y values and add
.then(function(values){
// `values` is an array of the messages from the
// previously resolved promises
// 'values' is an array of the results of the previous promise.all execution
return values[0] + values[1]; }); }// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
// 'fetchX()' and 'fetchY()' return respective promises
sum(fetchX(), fetchY())
// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(...) ` to wait for the
// resolution of that returned promise.
// We get the sum of the two promises and wait for the promise to execute successfully
.then(function(sum){
console.log(sum);
});
Copy the code
There are two layers of promise in this code. FetchX () and fetchY() are called directly, and the value they return (promise) is passed to sum(…). . The base value that this promise represents can be ready now or in the future. But each promise normalizes its behavior, and we reason about the values of X and y in a time-independent way. At some point in time, they’re a future value.
The second promise is sum(…). Created (by promise.all ([…] ), and return the promise. By calling then(…) To wait. When the sum (…). When the operation is complete, both promises passed in by Sum are executed and ready to print. Hidden here in sum(…) The logic that waits for future values of x and y.
Note: in this sum(…) Inside, this promise.all ([…] Call to create a promise (wait for promiseX and promiseY to resolve). Then the chain call.then(…) Method creates another Promise, then sums (values[0] + values[1]) and returns it.
Therefore, we have sum (…) Call then (…) at the end Method – actually runs on the second Promise returned, rather than by promise.all ([…] ) created a Promise. In addition, while the then method is not called at the end of the second Promise, a Promise is also created here.
Promise. Then (…). You can actually use two functions, the first for successful operations and the second for failed operations: if there is an error in getting x or y, or some kind of failure in adding, sum(…) The returned Promise is rejected and passed to then(…). The second callback error handler will receive the failure message from the Promise.
Externally, because promises encapsulate time-dependent states (waiting for completion or rejection of underlying values, promises themselves are time-independent), they can be composed in predictable ways without requiring the developer to care about timing or underlying outcomes. Once resolved, a Promise is now an immutable value externally — it can then be observed as many times as needed.
Chain calls are really useful for you:
function delay(time) {
return new Promise(function(resolve, reject){
setTimeout(resolve, time);
});
}
delay(1000)
.then(function(){
console.log("after 1000ms");
return delay(2000);
})
.then(function(){
console.log("after another 2000ms");
})
.then(function(){
console.log("step 4 (next Job)");
return delay(5000);
})
Copy the code
Call Delay (2000) to create a promise that will be implemented (fulfill) after 2000ms, then pass the first then(…) To receive the callback signal, which also returns a promise, via the second then(…) Promise to wait for 2000ms promise.
Note: Because a Promise can now safely be passed anywhere in the program because it appears to be resolved once it is resolved. Because it can’t be changed accidentally or maliciously, this is especially true when looking at a promise in multiple places. Immutability may sound like an academic topic, but it is actually one of the most fundamental and important aspects of promise design and should not be casually ignored.
Use need not Promise
An important detail about promises is determining whether a value is an actual Promise. In other words, does it behave like Promise?
We know that Promise is made by New Promise(…) Syntactically constructed, you might think that p instanceof Promise is a sufficiently determinable type, well, not really!
This is mainly because a Promise value can be received from another browser window, such as iframe, and that window or frame has its own Promise value that is different from the Promise value in the current window or frame, so the check will not recognize the Promise instance.
In addition, libraries or frameworks can optionally encapsulate their own promises instead of using native ES6 promises to implement them. In fact, there’s probably no Promise in the libraries of older browsers.
Catch errors and exceptions
If a javascript exception error (TypeError or ReferenceError) occurs during Promise creation, the exception will be caught and the Promise will be rejected. Such as:
var p = new Promise(function(resolve, reject){
foo.bar(); // `foo` is not defined, so error! 'foo' is not defined
resolve(374); // never gets here
});
p.then(
function fulfilled(){
// never gets here
},
function rejected(err){
// `err` will be a `TypeError` exception object
// from the `foo.bar()` line.});Copy the code
However, if you call then(…) What happens when a JS exception error occurs in a method? Even if it doesn’t get lost, you might find the way they’re handled a little surprising until you dig a little deeper:
var p = new Promise( function(resolve,reject){
resolve(374);
});
p.then(function fulfilled(message){
foo.bar();
console.log(message); // Never reached here
},
function rejected(err){
// Never reached here});Copy the code
It does look like the exception in foo.bar() is eaten, but it’s not. However, there are some deeper problems that we are not aware of. P.t hen (…). The call itself returns another Promise, which will be rejected by the TypeError exception.
Handle uncaught exceptions
Many would say there are better ways.
A common suggestion is that promises should add a done(…) , which essentially marks the Promise chain as “done.” Done (…). Does not create and return a Promise, so passes to Done (..) The callback to is obviously not going to report the problem to link promises that don’t exist.
The callback chain of a Promise object, whether ending in a then or catch method, may not be caught if the last method throws an error (because errors within a Promise do not bubble up globally). Therefore, we can provide a done method that is always at the end of the callback chain, guaranteed to throw any errors that might occur.
var p = Promise.resolve(374);
p.then(function fulfilled(msg){
// numbers don't have string functions,
// so will throw an error
console.log(msg.toLowerCase());
})
.done(null.function() {
// If an exception is caused here, it will be thrown globally
});
Copy the code
What’s changed in ES8? Async/await (Async/await)
JavaScript ES8 introduces async/await, which makes working with Promises easier. Here’s a quick look at the possibilities async/await offers and how to use them to write asynchronous code.
Declare asynchronous functions using async. This function returns an AsyncFunction object. The AsyncFunction object indicates that the code contained in the function is an asynchronous function.
When the async declaration function is called, it returns a Promise. When this function returns a value, which is just a normal value, a promise is automatically created inside the function and resolved using the value returned by the function. When this function throws an exception, the Promise is rejected by the thrown value.
An async function declaration can include an await symbol, which suspends execution of the function and waits for the resolution of the passed Promise to complete, then resumes execution of the function and returns the parsed value.
The purpose of async/ Wait is to simplify the use of promises
Take a look at the following:
// Just a standard JavaScript function
// Standard js
function getNumber1() {
return Promise.resolve('374');
}
// This function does the same as getNumber1
// This function does the same thing, returning a promise
async function getNumber2() {
return 374;
}
Copy the code
Similarly, the function throws an exception as if the promise returned by the function were rejected:
// These two functions are the same
function f1() {
return Promise.reject('Some error');
}
async function f2() {
throw 'Some error';
}
Copy the code
The await keyword can only be used in async functions, allowing de-synchronization to wait for a promise to execute. If you use promises outside of async, you still need to use then callbacks.
async function loadData() {
// `rp` is a request-promise function.
// 'rp' is a request promise function
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
// Now both requests are executed and must wait until their execution is complete
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
// Since, we're not in an `async function` anymore
// we have to use `then`.
// Since we are no longer in asynchronous functions, we must use 'then'
loadData().then((a)= > console.log('Done'));
Copy the code
Asynchronous functions can also be defined using asynchronous function expressions. Asynchronous function expressions are very similar to asynchronous function statements and have almost the same syntax. The main difference between an asynchronous function expression and an asynchronous function statement is the function name, which can be omitted from an asynchronous function expression to create anonymous functions. Asynchronous function expressions can be used as declarations (function expressions that are invoked immediately) and run as soon as they are defined.
Something like this:
var loadData = async function() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
Copy the code
More importantly, async/await is supported in all major browsers:
Write highly maintainable, stable asynchronous code
1. Simplify code
Less code can be written with async/await. Each time async/await is used, some unnecessary step is skipped: with.then, an anonymous function is created to process the response:
// `rp` is a request-promise function.
rp('https://api.example.com/endpoint1').then(function(data) {
/ /...
});
Copy the code
And:
// `rp` is a request-promise function.
var response = awaitThe rp (' HTTPS://api.example.com/endpoint1');
Copy the code
2. Error handling
Async/ Wait can use the same code structure (known as try/catch statements) to handle synchronous and asynchronous errors. See how it works with Promise:
function loadData() {
try { // Catches synchronous errors.
getJSON().then(function(response) {
var parsed = JSON.parse(response);
console.log(parsed);
}).catch(function(e) { // Catches asynchronous errors
console.log(e);
});
} catch(e) {
console.log(e);
}
}
view raw
Copy the code
And:
async function loadData() {
try {
var data = JSON.parse(await getJSON());
console.log(data);
} catch(e) {
console.log(e); }}Copy the code
3. Conditional processing
It is much easier to write conditional code with Async/WAIT:
function loadData() {
return getJSON()
.then(function(response) {
if (response.needsAnotherRequest) {
return makeAnotherRequest(response)
.then(function(anotherResponse) {
console.log(anotherResponse)
return anotherResponse
})
} else {
console.log(response)
return response
}
})
}
Copy the code
And:
async function loadData() {
var response = await getJSON();
if (response.needsAnotherRequest) {
var anotherResponse = await makeAnotherRequest(response);
console.log(anotherResponse)
return anotherResponse
} else {
console.log(response);
returnresponse; }}Copy the code
4. Error stack
Unlike async/await, the error stack returned from the Promise chain does not provide where the error occurred. Take a look at these:
function loadData() {
return callAPromise()
.then(callback1)
.then(callback2)
.then(callback3)
.then((a)= > {
throw new Error("boom");
})
}
loadData()
.catch(function(e) {
console.log(err);
// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});
Copy the code
And:
async function loadData() {
await callAPromise1()
await callAPromise2()
await callAPromise3()
await callAPromise4()
await callAPromise5()
throw new Error("boom");
}
loadData()
.catch(function(e) {
console.log(err);
// output
// Error: boom at loadData (index.js:7:9)
});
Copy the code
5. Debugging
If you’ve ever used promises, you know that debugging them can be a nightmare. For example, if you set a breakpoint in a program and then block and use a debug shortcut (such as “stop”), the debugger will not move below because it only executes synchronized code “step by step.” With async/ WAIT, you can step through wait calls as if they were normal synchronous functions.
Follow-up document translation will follow up!!
Welcome to the front of the Mystery public account: