JavaScript is aSingle threadedThe programming language meansOnly one task can be performed at the same time. However, modern web applications often need to handle a large number of asynchronous operations, such as network requests, file reading and writing, user input, etc. If all of these operations are synchronized, it will cause the application to be blocked while waiting for some operations to complete, and the user experience will become very poor.
To better handle these asynchronous operations, JavaScript introduced the concept of asynchronous programming. In synchronous programming, the code will be executed in sequence, and each operation must wait for the previous operation to complete before continuing. In contrast, asynchronous programming code is not executed sequentially, allowing the program to not have to wait for the task to complete when executing an operation, but can continue to execute subsequent code. Asynchronous programming is being processedNon-blocking I/O operation、Event-drivenand other aspects have played an important role inImprove performance、Avoid blockageand providing a better user experience brings advantages.
Callback function (Callback)
In JavaScript, the earliest asynchronous programming method was through callback functions. When a time-consuming operation needs to be performed, such as initiating a network request or reading a file, an asynchronous callback can be implemented by passing a function as a parameter to an asynchronous operation and calling the function after the operation is completed.
As the application becomes more complex, the nesting use of multiple asynchronous operations will result in the nesting of callback functions, forming the so-calledCallback Hell。
getData(function(result1) { processData(result1, function(result2) { processMoreData(result2, function(result3) { // ... There are more nested callbacks }); }); });
This nested structure makes the code highly coupled, difficult to understand and maintain, and prone to causing errors. In callback functions, it is not only difficult to clearly capture and handle errors, but also cannot be usedreturn
Statement. In order to overcome the problem of callback hell, a series of solutions emerged.
Observer mode
Observer mode
Defines the object betweenOne to manyDependency, when an object's state changes, all objects that depend on it will be notified and automatically updated. In JavaScript,Event listening is an implementation of the observer pattern。
Event listening is a common asynchronous programming method, especially suitable for event-triggered scenarios, such as browser DOM operations and NodeJS event-driven. Through event listening, the program can register a listener for a specific event and execute the corresponding callback function when the event occurs.
// In the browser// Event listening('myButton').addEventListener('click', function() { ('Button clicked!'); }); // Trigger event('myButton').click(); // Simulate button click event // inconst EventEmitter = require('events'); // themeclass Subject extends EventEmitter { setState(state) { ('stateChange', state); } } // Observerclass Observer { update(state) { ('Received updated state:', state); } } const subject = new Subject(); const observer = new Observer(); // Add observer to topic('stateChange', (observer)); ('newState');
RxJSIt is also an observer-based library that handles asynchronous and event-driven programming by using observables. For specific use, please refer toRxJS Documentation。
Observer pattern makes the code more powerfulModularandDecoupling. However, it is necessary to note that the entire program becomes an event-driven model, and the registration and triggering of the event listener are separated, which may lead toNot clear enough operation. When using observer mode, trade-offs need to be weighed to ensure that the code structure is clear and easy to understand.
Publish subscription model
Publish-Subscribe is aMessage Paradigm, it is also a way of handling asynchronous programming, similar to the observer pattern, but has an intermediate event channel, commonly known asEvent Bus. The sender of the message will not send the message directly to a specific recipient. Instead, divide the published messages into different categories without knowing which subscribers may exist. Similarly, subscribers can express interest in one or more categories, receive only messages of interest without knowing which publishers exist.
Compared with event listening, publish-subscribe modeLower coupling, allowing decoupling between publisher and subscriber. In,EventEmitter
The characteristics of both observer mode and publish subscription mode can be implemented, depending on how it is used.
Here is a simple implementation of the publish-subscribe mode:
const EventEmitter = require('events'); class EventBus extends EventEmitter {} const eventBus = new EventBus(); // Subscribersfunction subscriber(state) { ('Received updated state:', state); } // Add subscriber to event bus('stateChange', subscriber); // Publisherfunction publisher(newState) { ('stateChange', newState); } // Trigger state changepublisher('newState');
Both the publish subscription mode and the observer mode are effective ways to solve the callback hell problem. With its flexibility and loose coupling, the publish subscription model is more suitable for complex systems, while the observer model is simpler and suitable for smaller scenarios.
However, when dealing with multiple observers or subscribers, both patterns mayComplex code,Increase maintenance and understanding costs. Therefore, when selecting a pattern, it is necessary to weigh various factors based on project size and complexity to ensure the clarity and maintainability of the code.
Promise
Promise
It is a new asynchronous programming solution added in ES6. It is a complement to the callback function and can avoid the callback hell problem. Promise is essentially a container that holds the result of an event that will end in the future (usually an asynchronous operation).
A Promise object contains its prototype, status value (pending/fulfilled/rejected), value (value returned by the then method). The Promise object will be executed immediately after it is created, and the function execution will be blocked, and the function parametersresolve
andreject
When executing the , it will be hung to execute in the micro task.
Status value:
Pending (in progress): Indicates that the asynchronous operation has not been completed yet and is still in progress.
Fulfilled (completed): Indicates that the asynchronous operation has been successfully completed, and the result can be obtained through the then method.
Rejected (failed): Indicates that the asynchronous operation failed, and the error can be caught by the catch method or the second parameter in another then.
Core method:
resolve: Transform the Promise state from pending to fulfilled and pass the result value.
reject: Transform the Promise state from pending to rejected and pass the error message.
The basic syntax for creating a Promise object is as follows:
const promise = new Promise((resolve, reject) => { // Asynchronous operation, only by executing resolve/reject can the Promise state be determined, and no other operation can change this state. Once the state changes, it will not change again. if (/* Asynchronous operation succeeds */) { resolve(result); // Pass the result to then } else { reject(error); // Pass the result to the second parameter of then or catch } });
Promise'sChain callThrough the then method, the order and logic of asynchronous operations can be expressed more clearly:
promise .then(result => { // The successful processing results return result; }) .then(result2 => { // Handle result2 return result2; }) .then(result3 => { // Handle result3 return result3 }) .catch(error => { // Handle errors });
In Promise,then
The first parameter callback function of the method is used to handle the result of successful asynchronous operation, andthen
The second parameter callback of the method orcatch
The method is used to deal with asynchronous operation failures.
Promises are widely used to handle asynchronous operations in JavaScript, including AJAX requests, timing tasks, file operations, etc. Although Promise solves the problem of callback hell and other problems, there are someshortcoming:
First, the fn in the Promise cannot be canceled. Once it is newly created, it will be executed immediately and cannot be cancelled in the middle.
Second, if the callback function is not set, the errors thrown by Promise will not be reflected outside.
Third, when in the pending state, it is impossible to know which stage it is currently progressing (it is just beginning or about to be completed).
Fourth, the code is redundant. The original task is wrapped by Promise. No matter what operation is, it looks like a bunch of then at first glance, and the semantics become unclear.
Generator function
ES6 introducedGenerator function
It is a powerful asynchronous programming solution, and is actually an implementation of coroutines. As a state machine, the Generator function encapsulates multiple internal states by usingyield
andreturn
Expressions implement pause and recovery of execution. Execute the Generator function to return a traverser object through the object'snext
、return
andthrow
Methods can gradually traverse each state inside the Generator function. This feature makes asynchronous programming more intuitive and controllable.
yield expression: Marks the location where execution is suspended, and it will only be executed when the next method is called and the internal pointer points to the statement. It provides JavaScript with a manual "lazy evaluation" function.
function* gen() { yield 123 + 456; } const generatorInstance = gen(); // Lazy evaluation, triggering calculations only when needed(().value); // Output: 579
return expression: Mark the end position of the Generator function. After calling the return method, the execution of the Generator function will be terminated, and subsequent logic will no longer be executed.
function* myGenerator() { yield 1; return 2; // When the generator function terminates when the return is executed yield 3; // This statement will not be executed} const generator = myGenerator(); (()); // Output: { value: 1, done: false }(()); // Output: { value: 2, done: false }(()); // Output: { value: undefined, done: true }
(): Used to restore the execution of the Generator function, you can take a parameter, which will be regarded as the return value of the previous yield expression. The Generator function has a context state (context) that is unchanged from pausing to resuming operation. Through the parameters of the next method, there is a way to continue injecting values into the function body after the Generator function starts running. That is to say, different values can be injected from the outside to the inside at different stages of the Generator function running, thereby adjusting the function behavior.
function* f() { for(var i = 0; true; i++) { var reset = yield i; if(reset) { i = -1; } } } var g = f(); () // { value: 0, done: false } () // { value: 1, done: false } (true) // { value: 0, done: false }
(): Returns the given value and ends the traversal of the Generator function.
function* gen() { yield 1; yield 2; yield 3; } var g = gen(); () // { value: 1, done: false } ('foo') // { value: "foo", done: true } () // { value: undefined, done: true }
After the traverser object g calls the return() method, the value attribute of the return value is the parameter foo of the return() method. And, the traversal of the Generator function terminates. If no argument is provided when the return() method is called, the value attribute of the return value is undefined.
(): An error can be thrown outside the function body and then captured inside the Generator function body.
var g = function* () { try { yield; } catch (e) { ('Internal Capture', e); } }; var i = g(); (); try { ('a'); ('b'); } catch (e) { ('External Capture', e); } // Internal capture a// External capture b
The traverser object i throws two errors in succession. The first error is caught by a catch statement inside the Generator function body. i The second error was thrown. Since the catch statement inside the Generator function has been executed, the error will not be caught again, so the error is thrown out of the Generator function body and is caught by the catch statement outside the function body.
yield* expression: If inside the Generator function, call another Generator function. You need to manually complete the traversal within the former function body.
function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; // Manual traversal foo() for (let i of foo()) { yield i; } yield 'y'; } // Equivalent tofunction* bar() { yield 'x'; yield 'a'; yield 'b'; yield 'y'; } for (let v of bar()){ (v); } // x a b y
Manual traversal writing is very troublesome, es6 providesyield*
Expressions, used in aGenerator
Execute another functionGenerator
function.
function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; yield* foo(); yield 'y'; } for (let v of bar()){ (v); } // x a b y
Generator
Functions have a wide range of application scenarios, including but not limited to:
Iterator: Generator functions can be used to create custom iterators. Custom iterative logic can be easily implemented by generating the next value in each iteration using yield.
Asynchronous operation: Generator functions can be used in conjunction with yield to achieve clearer asynchronous code.
** Coroutine**: Generator functions can simulate simple coroutines, allowing pause and recovery during function execution.
Lazy calculation: Generator functions can be used to process large data streams, gradually generate and process data without loading all data at once, optimizing performance.
Streaming: Multi-step operations are time-consuming, and all steps are automatically performed in order using Generator.
Process the file content: When processing large files, Generator can read the file content line by line without having to read the entire file to memory at one time.
Save resources: In some cases, if the generated data is only used during the iteration process and does not need to be saved in memory, using Generator can save system resources.
While Generator avoids the complexity of callback hell and Promise chain calls, Generator makes the code more complex and difficult to understand, especially for beginners, it may take more time to adapt and understand this programming model.
Async/Await
ES2017 introducedAsync/Await
Syntax sugar brings a more intuitive and readable way to asynchronous programming. Async/Await is a complex of Generator and Promise, and it also comes with it.Automated Execution
,andawait
The heel isPromise
. Its syntactic sugar properties make asynchronous code look more like synchronous code, eliminating the hell problem of callbacks and improving readability and maintainability.
The basic structure is as follows:
async function myAsyncFunction() { // Asynchronous operation const result = await someAsyncOperation(); return result; }
async
Functions use keywordsasync
Declaration, internal useawait
Keywords wait for the asynchronous operation to complete. existasync
In the function,await
Pause function execution, waitPromise
Solve and return.
In terms of error handling, the async function adopts the traditionaltry-catch
Structure to make the code structure clearer:
async function fetchData() { try { const result1 = await getData(); const result2 = await processData(result1); const result3 = await processMoreData(result2); // ... Follow-up operations return finalResult; } catch (error) { // Handle errors } }
It should be noted that the Promise object returned by the async function will not change state after the Promise object after all the internal await commands are executed, unless a return statement or an error occurs during this process.
Overall, the implementation of async functions is the most concise and most semantic in line with the most semantic irrelevant code.
Summarize
These different asynchronous programming methods have their own advantages and disadvantages, and choosing the right method depends on the project's needs, complexity and the development team's experience. In practical applications, asynchronous programming methods can be flexibly selected according to specific circumstances to improve the quality and maintainability of the code.
The above is a detailed explanation of how JavaScript uses asynchronous decryption of callback hell. For more information about JavaScript asynchronous callbacks, please pay attention to my other related articles!