我们都去过一家餐厅。现在想象这种情况,一个服务员来到您的桌子上订单。这个服务员采用了一种独特的方法,在那里他们接您的订单,将其运送到厨房,等到准备就绪,然后将其提供给您。但是有一个接收器,服务员必须接收您的订单,将其交付给厨房,然后等到准备就绪,然后为您服务,这意味着他们之间没有其他任何事情。
现在,让我们考虑与其他服务员不同的情况。在这种情况下,服务员接您的订单并将其交给厨房,但他们不再等待厨师准备饭菜,而是立即继续提供其他桌子并接受其他订单。当他们这样做时,回到厨房里,厨师准备您的订单,当准备就绪时,即使他们目前正在为其他客户提供服务,
即使他们迅速将用餐交给服务员。示例一个和两个可以用来显示同步和异步编程的工作。近年来,JavaScript的受欢迎程度激增。截至2022年,大约98%的网站使用JavaScript作为客户端编程语言,使开发人员能够创建高性能和可扩展的应用程序。该语言处理异步编程能力的有效性极大地促进了该语言的成功。
默认情况下的JavaScript是同步的和单线程。同步编程意味着执行操作是顺序完成的。在执行上一个操作之前,不能执行操作。对于复杂的计算,这提出了一个问题。另一方面,异步编程是一项技术,计算机程序同时处理多个任务比依次执行它们,而不是独立于主程序流。
致电堆栈
呼叫堆栈跟踪程序中的多个函数调用本身就是数据结构。每次调用函数时,都会将新帧添加到呼叫堆栈中。当函数的执行结束时,也会从堆栈中删除框架,也会发生同样的情况。在异步编程的背景下,这至关重要,因为这可以帮助了解函数执行方式。要执行任何异步或同步代码,回调必须与回调相互作用,但不限于回调队列和事件循环。以下代码用一个简单的示例说明了这一点:
function greet(name) {
return `Hello, ${name}!`;
}
function hello() {
const greeting = greet("Nzioki");
console.log(greeting);
}
hello();
输出:
Hello, Nzioki!
代码故障
-
hello()被调用并添加到呼叫堆栈
-
现在,一旦进入Hello(),enger(“ nzioki”)被调用并添加
-
etch(“ nzioki”)完成,其框架从堆栈中删除,控制返回到Hello()
Hello()已完成,其框架从堆栈中删除
同步与异步执行
同步执行可用于简单和线性操作,因为任务是顺序处理的。每个任务必须在另一个开始执行之前完成,这可能是复杂操作的问题(例如,文件I/O或网络请求。这导致了差的用户体验。
另一方面,异步执行允许执行任务彼此独立。在启动异步操作的情况下,主要程序流不等待完成,它继续执行其他任务。一旦异步任务完成,您的程序就会随结果提供。考虑一个简单的同步函数,依次执行三个任务:打印“ A”,“ B”和“ C”,每个任务之间延迟一秒钟。
function synchronousExample() {
console.log("A");
console.log("B");
console.log("C");
}
synchronousExample();
输出:
A
B
C
在这种情况下,任务是顺序执行的,每个任务都必须在继续进行下一个任务之前完成。
现在,让我们使用SettiMeout实现同步的同步版本,以引入延迟。我们将在每个任务之间打印“ A”,“ B”和“ C”。
function asynchronousExecution() {
console.log("A");
setTimeout(() => {
console.log("B");
}, 3000);
setTimeout(() => {
console.log("C");
}, 1000);
}
asynchronousExecution();
输出:
A
C
B
在异步执行中,主线程不会等待Settimeout函数完成其任务。相反,它在调用后立即进行下一个声明。结果,首先打印了“ A”,然后在一秒钟后打印了“ C”,最后三秒钟后,“ B”打印了。
同步与异步执行时间表
以视觉上说明同步执行和异步执行之间的差异,请考虑以下时间表图:
同步执行时间表:
Time: 1s 2s 3s
| | |
| | |
Task: A -> B -> C
在同步执行中,每个任务都会阻止主线程直到完成,创建顺序流。
异步执行时间表:
Time: 1s 2s 3s
| | |
| | |
Task: A -> | |
|-> C |
|-> B
在异步执行中,启动任务并继续在后台运行,而主线程不再等待它们完成。
1.回调
回调是一个函数,作为参数传递给另一个函数,并在调用后稍后执行。回调的主要目的是响应事件执行代码。在JavaScript中,回调允许开发人员指定任务完成执行后应该发生的事情。
让我们考虑一个简单的示例,其中我们拥有一个函数getUserData,该函数使用SettiMeout模拟从服务器中获取用户数据。我们提供一个回调函数OnuserDatafetch,一旦数据准备就绪,可以处理结果。
function getUserData(callback) {
setTimeout(() => {
const user = { id: 001, name: "Nzioki Dennis" };
callback(user);
}, 1000);
}
function onUserDataFetched(user) {
console.log("User data:", user);
}
getUserData(onUserDataFetched);
输出(1秒后):
User data: { id: 001, name: "Nzioki Dennis" }
在此示例中,getUserData函数将回调函数回调作为参数。在GetUserData中,我们使用SettiMeout模拟了需要一秒钟的异步操作。超时完成后,我们调用回调函数,将用户数据作为参数传递。
回调地狱
有时JavaScript操作会增长并需要多个异步操作,这会导致嵌套的回调彼此下方,形成“厄运的金字塔”。我们称这种厄运金字塔的原因是,每个回调都必须依赖/等待上一个回调。这会影响代码的可维护性和可读性。这是一个演示回调地狱的示例:
function asyncTask1(callback) {
setTimeout(() => {
console.log("Task 1 completed");
callback();
}, 1000);
}
function asyncTask2(callback) {
setTimeout(() => {
console.log("Task 2 completed");
callback();
}, 1500);
}
function asyncTask3(callback) {
setTimeout(() => {
console.log("Task 3 completed");
callback();
}, 500);
}
asyncTask1(() => {
asyncTask2(() => {
asyncTask3(() => {
console.log("All Tasks completed.");
});
});
});
输出:
Task 1 completed
Task 2 completed
Task 3 completed
All Tasks completed.
您如何避免回调地狱?
-
使用承诺或异步/等待:拥抱承诺或异步/等待更好地控制异步操作。
-
模块化代码:将复杂的任务分解为较小的可管理功能。
-
使用错误处理:实现错误处理机制以优雅地捕获和处理错误。
-
避免嵌套回调:避免在彼此之间嵌套多个回调。
-
命名函数:使用命名的函数来提高可读性。
-
关注点的分离:保持不同的功能分开以减少回调嵌套。
-
库和模块:使用设计用于处理异步操作的库或模块。
-
使用控制流库:考虑使用诸如async.js或功能编程概念之类的控制流库来管理异步流。
-
prosisify函数:将基于回调的功能转换为一致处理的承诺。
-
避免匿名函数:使用命名函数代替匿名函数来提高代码清晰度。
-
重构和简化:定期重构代码以简化和减少回调链。
2.承诺
承诺对象表示异步操作的最终完成/失败及其结果值,并且可以使用.catch()和.catch()方法或使用异步/等待语法来链接。在这三个状态之一中的任何一种时期都存在诺言:
-
未决:创建承诺通常是在待处理的。
-
实现(已解决):如果由承诺代表的异步操作完成,则承诺过渡到满足的状态。
-
拒绝:如果异步操作失败,则向被拒绝状态的承诺过渡。
创造诺言
我们利用承诺构造函数,该构建体接受函数作为参数,以创建承诺。此功能的参数,也称为执行函数,解决和拒绝。在执行程序函数中,我们执行异步任务,当成功完成时,我们将其调用结果。如果发生错误,我们将使用错误对象调用拒绝。
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
if (/* operation successful */) {
resolve(result);
} else {
reject(error);
}
});
保证链条
借助Promise Chanding,您可以连续进行几项异步操作,其中一个操作作为下一个操作的输入。
function asyncTask1() {
return new Promise((resolve, reject) => {
// ...
});
}
function asyncTask2(resultFromTask1) {
return new Promise((resolve, reject) => {
// ...
});
}
asyncTask1()
.then(result1 => asyncTask2(result1))
.then(finalResult => {
// Handle final result
})
.catch(error => {
// Handle errors
});
承诺
Promise.all()接受诺言清单作为输入。
const promises = [asyncTask1(), asyncTask2(), asyncTask3()];
Promise.all(promises)
.then(results => {
// Handle all results
})
.catch(error => {
// Handle errors
});
3.异步/等待
异步/等待在承诺之上建立,并消除了承诺链的需求。 JavaScript中的“异步”和“等待”术语用于创建看起来更同步和可读性的异步代码。使用“ async”关键字声明异步函数,并且可以使用“等待”关键字暂停其执行,直到解决或拒绝承诺为止。在异步函数中,“等待”关键字用于调用返回承诺的函数,有效地同步执行流并使异步代码易于理解。以下示例显示了异步/等待的工作方式:
// An asynchronous function that returns a Promise after a delay
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// An async function using await to pause execution
async function asyncTask() {
console.log("Task started");
await delay(2000); // Pause here until the Promise is resolved
console.log("Task completed");
}
// Call the async function
asyncTask();
输出
Task started
[After a 2-second delay]
Task completed
在上面的代码段中,异步函数演示了异步/等待的工作方式。等待关键字暂停了函数的执行,直到延迟承诺在2秒延迟后解决。
用异步/等待
处理错误异步/等待还提供了一种方便的方法来处理异步代码中的错误。异步/等待使使用try-Catch块直接处理错误。如果异步函数中发生错误,则将捕获在捕获块中,从而可以进行干净和集中的错误处理。
与异步/等待的错误处理示例
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getUserData() {
await delay(1000);
throw new Error("Failed to fetch user data.");
}
async function fetchUserData() {
try {
const user = await getUserData();
console.log("User data:", user);
} catch (error) {
console.error("Error fetching user data:", error.message);
}
}
fetchUserData();
输出
Error fetching user data: Failed to fetch user data.
getUserData函数会引发错误。 fetchuserdata函数使用try-catch块捕获错误,从而使我们可以优雅地处理。
4.异步代码中的错误处理
JavaScript异步编程的关键组件是错误处理。在处理异步过程以保持应用程序稳定性并为用户提供有用的反馈时,适当处理问题至关重要。在本节中,我们将探讨各种错误处理策略和技术,以处理异步代码中的错误。
1。使用.catch()
使用承诺承诺提供了一种使用.catch()方法处理错误的简单方法。当拒绝承诺时,控件将传递给最近的.catch()块,您可以处理错误。
function fetchUserData() {
return fetch('https://api.example.com/user')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch user data.');
}
return response.json();
})
.catch(error => {
console.error('Error fetching user data:', error.message);
});
}
fetchUserData();
使用fetch()函数,fetchuserdata函数从API检索用户数据。如果响应状态不正确(例如,HTTP状态代码404),则使用throw new错误()拒绝了错误。这。 catch()块然后捕获错误并将消息记录到控制台。
2。使用async/等待try-catch:
尝试键入块使使用异步/等待时更清晰,更容易理解错误。在异步函数中,您可以使用try-catch捕获和处理等待操作期间发生的错误。
async function fetchUserData() {
try {
const response = await fetch('https://api.example.com/user');
if (!response.ok) {
throw new Error('Failed to fetch user data.');
}
const userData = await response.json();
console.log('User data:', userData);
} catch (error) {
console.error('Error fetching user data:', error.message);
}
}
fetchUserData();
fetchuserdata函数使用异步/等待用户数据。如果在提取或JSON解析错误时发生错误,则将其捕获在TryCatch块中并登录到控制台。
3。用Promise.all()处理多个承诺:
使用多个异步操作运行,所有承诺都完成,Promise.all()使您可以通过解决或拒绝其返回的承诺来共同处理错误。
async function fetchDataFromMultipleSources() {
const [data1, data2, data3] = await Promise.all([
fetch('https://api.example.com/data1'),
fetch('https://api.example.com/data2'),
fetch('https://api.example.com/data3')
]);
const jsonPromises = [data1.json(), data2.json(), data3.json()];
const [parsedData1, parsedData2, parsedData3] = await Promise.all(jsonPromises);
console.log('Data 1:', parsedData1);
console.log('Data 2:', parsedData2);
console.log('Data 3:', parsedData3);
}
使用fetch()从多个端点获取数据,然后等待使用Promise解决的所有获取承诺。全部()。如果任何提取操作都失败了,则promise.ly()返回的承诺将被拒绝,如果使用的话,该错误将被捕获在捕获块中。
5.并发模型和事件循环
并发模型
如前所述,默认情况下JavaScript是一种单线程语言,具有一个负责处理所有任务的执行线程。这是多线程语言的对立面,该语言同时执行独立任务。并发是指系统同时处理多个任务的能力,而无需同时执行它们。
那么,单线程语言的JavaScript如何实现并发呢?它通过其非阻滞行为来做到这一点。异步操作(例如网络请求,文件I/O或计时器)在后台执行,而主线程继续执行剩余代码。回调函数或解决的承诺提醒事件循环,它可以在完成异步操作后运行相关回调。
事件循环
JavaScript并发的关键部分,事件循环只要程序执行并维护应用程序的响应能力,就可以连续运行。事件循环遵循每次迭代期间的特定相位序列。他们总共6个;
-
计时器:此阶段中的事件循环检查并使用Settimeout()和SetInterval()和SetInterval()。
(
-
i/o回调:执行I/O相关回调,例如网络请求,文件系统操作或其他涉及已完成的I/O的异步任务。<<<<<<<<<<<<<<<<<<<<<<< /p>
-
等待/准备:在此阶段,事件循环等待将新的I/O事件添加到队列中。简而言之,是内部维护的阶段
-
i/o轮询:检查自上次迭代以来发生的I/O事件并执行其回调。
-
setimediate()回调:也称为检查阶段,此阶段执行setimMediate()回调
-
关闭事件:最终阶段处理清理任务。它处理回调,例如关闭事件回调,例如关闭数据库连接
让我们考虑事件循环如何运行的简化说明:
+---------------------------+
| |
| V
+- Poll ---------------------> Check ------------------> Timers -----------+
| ^ | | | |
| | | | | |
| | v | v |
| +--- Pending Callbacks --+ +--- Microtasks--+
| |
+-------------------------------------------------------------------------+
6.绩效注意事项
JavaScript的异步编程功能可以显着提高应用程序的性能和响应能力。不过,它还提出了几个与性能相关的变量,开发人员应该意识到以确保最佳的资源和代码执行。
1。避免不必要的异步操作
虽然异步操作可能是有益的,但不必要的使用可能会影响性能。执行简单的任务并可以同步执行时,请尝试避免使用异步调用。过度使用异步操作可能会增加其他复杂性,并可能使代码库更复杂。
2。最小化回调嵌套(回调地狱)
过多使用回调,通常称为“回调地狱”,可能会导致难以阅读和可维护的代码。异步代码必须以减少嵌套并提高可读性的方式编写。如果您想压缩回调链并改善代码组织,请考虑使用诺言或异步/等待。
3。限制和拒绝异步操作
考虑在启动许多异步操作(例如管理用户输入事件)的情况下采用节流或拒绝策略。拒绝将函数的执行延迟至一定的不活动时期,而节流限制了在特定时间范围内可以调用函数的次数。撤销是一种编程技术,在包括前端Web开发在内的各种情况下都用于管理特定操作或功能的频率,以响应快速或频繁的事件,例如用户输入。
4。有效的错误处理
为了保持稳定,必须正确处理错误处理。但是,处理具有许多层次嵌套的回调链中的错误可能会导致性能开销。为了防止不必要的错误处理代码重复,同时使用承诺或异步/等待,请考虑集中错误处理。
5。优化异步API
设计自定义异步API时,旨在简单和易用。精心设计的API可以提高代码可读性并降低错误的可能性。考虑提供清晰的文档并使用直观函数名称使API更加友好。
7. JavaScript中异步编程的最佳实践
正确使用时,异步编程可以有效,有效。但是,如果无法正确处理,它也可能导致代码复杂性和微妙的错误。在JavaScript中使用异步代码时,以下是一些最佳实践:
1。使用承诺或异步/等待
而不是使用常规回调来处理异步动作,而是使用诺言或异步/等待。通过消除回调地狱并简化了错误处理,它们提供了一种更有条理和可读的方法来处理异步任务。
2。优雅地处理错误
始终处理异步代码中的错误。使用.catch()带有承诺或带有异步/等待的键入块,等待适当地处理和处理错误。通过这样做,您可以确定您的应用程序将保持可靠,并在出现问题时为用户提供有用的反馈。
3。限制全局变量的使用
避免使用全局变量存储可能异步修改的数据。而是使用函数参数或闭合来传递函数之间的数据。全球变量可以导致比赛条件并使代码更难维护。
4。油门和审阅异步电话:
对于经常触发的异步操作(例如,处理用户输入),请考虑限制或拒绝功能调用以控制执行速率并防止过度资源使用。
5。使用网络工人进行CPU密集型任务:
对于计算密集型任务,请考虑将工作卸载给网络工人。这种方法可防止主线程变得无反应并提供更好的用户体验。
6。避免阻止事件循环:
避免长期运行的同步操作,以阻止事件循环。异步代码不应用作规避昂贵同步任务的一种方式。
7。优化网络请求:
尽可能最大程度地减少网络请求的数量,并尽可能优化其大小。考虑使用HTTP压缩和缓存等技术来减少响应时间和带宽的使用。
8。在异步操作后清理:
在异步操作后勤奋清理。关闭文件,释放资源并取消订阅事件听众,以防止内存泄漏。
9。测试异步代码:
为异步功能编写详尽的单元测试,以确保它们在各种情况下的表现如预期的,包括成功的执行和错误处理。
10。文档异步API:
设计自定义异步API时,提供清晰简洁的文档。说明API的目的,预期输入和返回数据的结构。有据可查的API易于集成并提高代码可读性。
现实世界的例子
从API获取数据
让我们考虑一个常见的方案,其中Web应用程序需要从多个API获取数据并将组合结果显示给用户。在此示例中,我们将使用GitHub API获取有关用户及其存储库的信息。
async function fetchUserData(username) {
const userResponse = await fetch(`https://api.github.com/users/${username}`);
const userData = await userResponse.json();
return userData;
}
async function fetchUserRepositories(username) {
const repoResponse = await fetch(`https://api.github.com/users/${username}/repos`);
const repositories = await repoResponse.json();
return repositories;
}
async function displayUserDetails(username) {
try {
const [userData, repositories] = await Promise.all([
fetchUserData(username),
fetchUserRepositories(username)
]);
console.log('User Details:', userData);
console.log('Repositories:', repositories);
} catch (error) {
console.error('Error fetching data:', error.message);
}
}
displayUserDetails('octocat');
在此示例中,我们具有三个异步功能:fetchuserdata,fetchuserrepositories和displayuserdetails。每个功能都在等待从GitHub API获取数据,使代码看起来同步且易于阅读。
DisplayUserDetails函数使用Promise.All()同时获取用户数据和存储库。这种方法可确保同时进行两个API调用,从而改善整体性能并减少用户的等待时间。
实时聊天应用程序
考虑一个实时聊天应用程序,用户可以立即交换消息。这样的应用需要有效处理异步事件以提供无缝响应迅速的用户体验。
// WebSocket server setup using Node.js and ws library
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// Keep track of connected clients
const clients = new Set();
// WebSocket connection event
wss.on('connection', (ws) => {
// Add the new client to the set of connected clients
clients.add(ws);
// WebSocket message event
ws.on('message', (message) => {
// Broadcast the received message to all connected clients
for (const client of clients) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
});
// WebSocket close event
ws.on('close', () => {
// Remove the disconnected client from the set of connected clients
clients.delete(ws);
});
});
在此示例中,我们使用node.js和WS库来创建WebSocket服务器。 WebSocket是一种通信协议,可以通过单个长期的连接来启用客户端和服务器之间的双向数据传输。
客户端建立Websocket连接时,服务器将其添加到客户端集。每当从任何客户端收到消息时,服务器都会将消息广播给所有已连接的客户端(发件人除外)。这种方法可确保将消息实时传递给所有参与者。
Websocket的事件驱动的性质使其非常适合构建实时应用程序,因为它允许服务器同时处理多个异步事件而无需阻止主线程。这导致了一个高度响应迅速的聊天应用程序,该应用程序会立即向所有用户发送消息。
包起来!
异步编程是JavaScript的一个基本方面,它允许开发人员执行非阻滞操作,从而使应用程序更加响应和高效。通过利用承诺和异步/等待的技术,开发人员可以编写干净有组织的代码,以结构化的方式处理异步任务。此外,了解事件循环和JavaScript中的并发模型可帮助开发人员优化代码执行和资源利用率
快乐编码