首页 > 基础资料 博客日记
超硬核五千字!彻底讲明白JavaScript中的异步和同步,以及JavaScript代码执行顺序
2024-10-21 05:00:09基础资料围观78次
同步操作和异步操作是编程中处理任务的两种不同方式,它们主要区别在于控制流和对程序执行的影响。不知道大家是怎么理解JavaScript中的同步和异步的?JavaScript的代码执行顺序是怎么样?下面这段代码是同步还是异步的?
console.log('Start');
const response = await fetch('https://juejin.cn');
const data = await response.json();
console.log('Data received:', data);
console.log('End');
同步操作 (Synchronous)
定义: 同步操作会阻塞当前线程或进程,直到该操作完成。这意味着当一段代码执行同步操作时,后面的代码必须等待该操作完成才能继续执行。在同步操作中,任务按顺序执行,一个任务必须完成后才能执行下一个任务。这种方式会阻塞程序的执行,直到当前任务完成。
特点:
- 简单直观,代码按照从上到下的顺序执行。
- 在执行耗时操作时会阻塞其他任务,可能导致UI无响应(在前端JavaScript中)。
JavaScript示例:
console.log('Start');
function synchronousTask() {
for (let i = 0; i < 1000000000; i++) { /* 耗时任务 */}
console.log('Task done');
}
synchronousTask();
console.log('End');
在这个例子中,循环是一个简单的模拟耗时操作。当这段代码执行时,console.log('Start');
先执行并打印 Start
,synchronousTask();
开始执行,直到循环结束,才会打印出 “End”。在此期间,无法处理其他任务或用户交互。
异步操作 (Asynchronous)
定义: 异步操作允许程序在等待某个操作完成的同时继续执行其他任务,不阻塞当前线程。异步操作通常用于处理I/O操作、网络请求、定时器等潜在的长时间操作。
特点:
- 程序可以继续执行其他任务,提高应用的响应性和性能,特别是在处理I/O密集型任务时。
- 代码逻辑更复杂,需要回调函数、Promises或async/await来处理异步流程。
我们从代码中理解异步,这里使用一个定时器,JavaScript示例:
console.log('Start');
function asynchronousTask() {
setTimeout(() => {
console.log('Task done');
}, 1000);
}
asynchronousTask();
console.log('End');
在这段代码中,执行顺序如下所示:
console.log('Start');
先执行并打印Start
asynchronousTask();
开始执行,setTimeout
设置了一个异步任务,这个任务在 1 秒后执行,但不会阻塞程序console.log('End');
立即执行并打印End
- 1 秒后,异步任务完成,
console.log('Task done');
执行并打印Task done
但是一般在写代码的时候,我们的异步操作肯定不是通过定时器完成的,下面给大家展示两种异步操作写法,分别如下所示:
1、使用 Promise
处理异步操作
console.log('Start');
function asynchronousTask() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task done');
}, 1000);
});
}
asynchronousTask().then(message => {
console.log(message);
});
console.log('End');
在这个例子中:
asynchronousTask
返回一个Promise
,它在 1 秒后解决(resolve)console.log('Start');
和console.log('End');
立即执行- 1 秒后,
Promise
被解决,console.log(message);
打印Task done
2、使用 async/await
处理异步操作
console.log('Start');
async function runAsyncTask() {
const message = await asynchronousTask();
console.log(message);
}
runAsyncTask();
console.log('End');
在这个例子中:
async
函数runAsyncTask
内部使用await
等待asynchronousTask
完成,但不会阻塞外部代码console.log('Start');
和console.log('End');
立即执行- 1 秒后,
asynchronousTask
完成,console.log(message);
打印Task done
现在是不是对同步和异步的概念有了基本的认识,咱们再回到文章最开始的代码,结论是那段代码不是同步的,而是异步的。在上面代码中,遇到 await 等待结果阻塞代码执行了,为什么不说它是同步?
为了更好地理解,可以将这段代码放在一个异步函数中执行:
async function fetchData() {
console.log('Start');
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log('Data received:', data);
console.log('End');
}
fetchData();
我们先理解一下这个关键词:await
关键字会暂停当前的异步函数,等待异步操作完成,但不会阻塞外部代码,不会阻塞事件循环。
在这个例子中,fetchData
是一个异步函数,尽管await
关键字会暂停它的执行,但它不会阻塞整个程序的执行。因此,这段代码是异步的,而不是同步的。假设fetchData
函数后面有一个输出,它可能先执行的。
await
仅会暂停当前异步函数的执行,让出控制权给事件循环,以便处理其他任务。
当一个异步函数遇到 await
时,它会暂停该函数的执行,等待 Promise 被解决(resolved)或者被拒绝(rejected)。然而,这种暂停并不等同于传统的同步阻塞,因为其他任务依然可以执行。
咱们接下来通过一段代码来展示什么是阻塞整个程序的执行。在JavaScript中,阻塞代码通常是指那些在执行时会阻塞事件循环,导致其他任务无法执行的代码。常见的例子包括使用同步的I/O操作或长时间运行的计算。
以下是一个使用同步I/O操作的例子,这种操作会阻塞整个程序的执行:
const fs = require('fs');
function readFileSync() {
// 打印"Start"到控制台
console.log('Start');
// 同步读取文件的内容
// 在文件读取完成之前,这行代码会阻塞事件循环,其他任务无法执行
const data = fs.readFileSync('/opt/xiaodou.txt', 'utf8');
// 打印读取到的文件内容到控制台
console.log('File data:', data);
// 打印"End"到控制台
console.log('End');
}
readFileSync();
在这个例子中,fs.readFileSync
是一个同步的文件读取操作,它会阻塞事件循环,直到文件读取完成。在读取文件的过程中,其他任务无法执行,整个程序的执行被阻塞。
这段代码的执行顺序是严格线性的,整个程序会在读取文件时被阻塞,直到读取操作完成。
为了对比,可以看一下使用异步I/O操作的代码,这样的代码不会阻塞事件循环:
const fs = require('fs');
function readFileAsync() {
console.log('Start');
// 读取文件的异步操作,这不会阻塞事件循环
fs.readFile('/opt/xiaodou.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log('File data:', data);
});
console.log('End');
}
readFileAsync();
在这个例子中,fs.readFile
是一个异步的文件读取操作,它不会阻塞事件循环,程序可以继续执行其他任务。
现在对JavaScript中的同步是不是有一些理解:导致整个程序的执行被阻塞是同步,只是暂停当前函数执行是异步,使用await
关键字时,它会暂停当前异步函数的执行,等待异步操作完成,但不会阻塞事件循环,其他任务可以继续执行。
我们来看一下阻塞整个程序的代码,不仅会阻塞当前函数,还会阻塞整个事件循环,影响所有其他任务的执行,咱们现在在上面的同步函数中增加一个定时器,那么你猜猜定时器还能定时执行吗?
修改后的代码如下所示:
const fs = require('fs');
function readFileSync() {
console.log('Start');
// 设置一个定时器,计划在1秒后执行
setTimeout(() => {
console.log('Timer executed');
}, 1000);
// 同步读取文件的操作,这会阻塞事件循环
const data = fs.readFileSync('/opt/xiaodou.txt', 'utf8');
console.log('File data:', data);
console.log('End');
}
readFileSync();
在这个例子中,fs.readFileSync
是一个同步操作,它会阻塞事件循环,假设文件在1秒后无法读取完成,那么定时器无法在预定的1秒后执行。只有当文件读取操作完成后,定时器才会有机会执行。
事件循环是啥?
在上面我们一直提到事件循环,事件循环(Event Loop)是JavaScript运行时的一个重要机制,它允许非阻塞的异步编程,即使JavaScript是单线程的。事件循环负责管理和调度异步操作的执行,使得异步任务能够在任务完成后被处理,而不会阻塞主线程。
事件循环的工作原理
调用栈(Call Stack):
- JavaScript引擎有一个调用栈,这是一个LIFO(后进先出)数据结构,用于跟踪正在执行的函数。
- 当一个函数被调用时,它会被推入调用栈,当函数执行完成后,它会从调用栈中弹出。
任务队列(Task Queue):
- 任务队列是一个FIFO(先进先出)数据结构,用于存储等待执行的回调函数。
- 异步操作(如定时器、网络请求、事件处理等)完成后,其回调函数会被放入任务队列中。
事件循环(Event Loop):
- 事件循环不断地检查调用栈是否为空。
- 如果调用栈为空,并且任务队列中有待处理的任务,事件循环会将任务队列中的第一个任务移到调用栈中执行。
是不是感觉很难理解呢?接下来咱们结合代码理解一这一块的内容。
1、简单的示例
还是使用定时器的代码示例,说明一下事件循环的工作原理:
console.log('Start');
setTimeout(() => {
console.log('Timer executed');
}, 1000);
console.log('End');
执行过程:
输出结果:
Start
End
Timer executed
2、复杂的代码
让我们看一个更复杂的例子,包含多个异步操作:
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 500);
setTimeout(() => {
console.log('Timeout 2');
}, 100);
Promise.resolve().then(() => {
console.log('Promise 1');
});
Promise.resolve().then(() => {
console.log('Promise 2');
});
console.log('End');
执行过程:
输出结果:
Start
End
Promise 1
Promise 2
Timeout 2
Timeout 1
同学们,是不是发现个有趣的东西,这里多了一个微任务队列,那么微任务队列是什么?和任务队列是什么关系?
微任务和宏任务又是啥?
在JavaScript中,微任务(microtasks)和宏任务(macrotasks)是事件循环中处理异步任务的两种类型。它们代表了不同优先级的任务队列,事件循环会按照特定的顺序执行这些任务。
宏任务(Macro Task)
这个实际上就是咱们之前提到的任务队列中的任务。宏任务是那些在每个执行栈执行完毕后,从事件队列中取出并执行的任务。宏任务的执行频率低于微任务,因为它们需要等待整个执行栈清空后再执行。它们通常包括以下几类操作:
setTimeout
setInterval
setImmediate()
(Node.js特有)requestAnimationFrame()
(浏览器特有)I/O
操作- UI渲染(浏览器)
- 其他一些异步任务
宏任务会被推入到任务队列中,事件循环会从任务队列中依次取出宏任务并执行。简单记忆就是:需要较长时间才来完成的任务,毕竟宏伟的工程往往需要很长的时间。
微任务(Microtasks)
微任务是一些需要在当前任务执行完毕后立即执行的任务。它们通常是由Promises、process.nextTick
(Node.js特有)或者MutationObserver
等机制产生的。微任务队列优先级高于宏任务队列,这意味着在每个宏任务执行完毕后,事件循环会优先处理微任务队列中的所有任务,然后再处理下一个宏任务。它们通常包括以下几类操作:
- Promises的
.then()
、.catch()
和.finally()
回调 process.nextTick()
(Node.js)queueMicrotask()
(浏览器和Node.js)MutationObserver
(用于监听DOM变化)
微任务会被推入到微任务队列中,事件循环在每次执行完一个宏任务后,会检查并执行所有微任务,直到微任务队列为空。简单速记:微任务是指哪些需要尽快被完成的任务,小事要尽快做,拖久了就变大事了
干说有点费劲,给同学们来张流程图描述事件循环的基本原理:
这是一个简化的时序图,事件循环的主要步骤如下:
-
开始事件循环:JavaScript引擎开始执行代码。
-
同步任务队列:引擎首先检查同步任务队列(实际上是调用栈中的任务),执行队列中的任务。
-
执行同步任务:引擎开始执行同步任务,直到队列为空。
-
检查微任务队列:同步任务执行完毕后,引擎检查微任务队列。
-
执行微任务:如果有微任务(如Promise的
.then()
回调),引擎会执行这些微任务。 -
检查宏任务队列:微任务执行完毕后,引擎检查宏任务队列。
-
执行宏任务:如果有宏任务(如
setTimeout
回调),引擎会执行这些宏任务。执行完毕后再次检查微任务队列。 -
等待新任务:如果宏任务执行完毕,引擎会等待新的任务(如新的异步操作或宏任务)。
-
循环:引擎会不断地循环执行上述步骤。
同学们这时候会不会有很多疑问,如果觉得上面的文字还不好理解,咱们接下来跟着代码走一遍。
代码示例讲解
咱们还是在上面代码的基础上做修改,这里增加一个包含await
的方法,代码如下所示:
console.log('Start'); // 同步任务,立即执行
setTimeout(() => {
console.log('Timeout 1'); // 宏任务,500ms后执行
}, 500);
setTimeout(() => {
console.log('Timeout 2'); // 宏任务,100ms后执行
}, 100);
async function asyncFunction() {
console.log('Async Function Start'); // 同步任务,立即执行
await Promise.resolve().then(() => {
console.log('Inside Async Function - Promise Resolved'); // 微任务,放入微任务队列
});
console.log('Async Function End'); // 微任务,放入微任务队列
}
asyncFunction(); // 调用异步函数
Promise.resolve().then(() => {
console.log('Promise 1'); // 微任务,放入微任务队列
});
Promise.resolve().then(() => {
console.log('Promise 2'); // 微任务,放入微任务队列
});
console.log('End'); // 同步任务,立即执行
时序图
详细解释一下代码中的任务都有哪些:
同步任务立即执行:
console.log('Start');
立即执行,输出 “Start”。setTimeout(..., 500)
:注册一个500ms后的宏任务。setTimeout(..., 100)
:注册一个100ms后的宏任务。- 调用
asyncFunction()
:console.log('Async Function Start');
立即执行,输出 “Async Function Start”。await Promise.resolve().then(...)
:注册一个微任务。
Promise.resolve().then(...)
:注册两个微任务。console.log('End');
立即执行,输出 “End”。
微任务执行:
console.log('Inside Async Function - Promise Resolved');
(在asyncFunction
中的微任务)console.log('Async Function End');
(在asyncFunction
中的微任务)console.log('Promise 1');
(在主代码块中的微任务)console.log('Promise 2');
(在主代码块中的微任务)
宏任务执行:
Timeout 2
:100ms后输出 “Timeout 2”。Timeout 1
:500ms后输出 “Timeout 1”。
代码运行输出结果如下所示:
Start
Async Function Start
End
Inside Async Function - Promise Resolved
Async Function End
Promise 1
Promise 2
Timeout 2
Timeout 1
脑洞大开,搞事情
咱们能写一个阻塞事件循环的函数吗?
现在我们已经了解了基本原理,那么我们肯定可以搞点事情,我们可以使用循环阻止其他任务的执行,如下所示,这个函数会在指定的时间内阻塞事件循环,使得其他任务(包括异步操作和定时器)无法执行。
function blockEventLoop(duration) {
const startTime = Date.now();
while (Date.now() - startTime < duration) {
// 这个循环会一直运行,直到达到指定的持续时间
}
console.log(`Blocked for ${duration} ms`);
}
// 示例用法
console.log('Start');
setTimeout(() => {
console.log('Timeout executed');
}, 1000);
blockEventLoop(3000); // 阻塞事件循环3秒
console.log('End');
我们来分析一下执行过程
-
开始执行:
console.log('Start');
被推入调用栈,执行并输出 “Start”,然后从调用栈中弹出。 -
设置定时器:
setTimeout
被推入调用栈,设置一个1秒后的定时器。定时器的回调函数被注册,并计划在1秒后执行。最后setTimeout
从调用栈中弹出。 -
阻塞事件循环:
blockEventLoop(3000)
被推入调用栈,开始执行。进入while
循环,持续3秒,期间事件循环被阻塞,无法处理其他任务。3秒后,while
循环结束,输出 “Blocked for 3000 ms”。最后blockEventLoop
从调用栈中弹出。 -
继续执行:
console.log('End');
被推入调用栈,执行并输出 “End”,然后从调用栈中弹出。 -
事件循环处理定时器:由于事件循环被阻塞了3秒,1秒后的定时器未能在预定时间执行。事件循环恢复后,定时器的回调函数被移到调用栈中执行,输出 “Timeout executed”。
代码输出结果如下所示,不知道你对了吗?
Start
Blocked for 3000 ms
End
Timeout executed
注意事项
阻塞事件循环会导致所有其他任务无法执行,不能瞎搞,尤其是在处理用户界面或需要高响应性的应用程序中。不然会被用户骂的。
拓展:JavaScript和Java在同步和异步编程上的差异
因为我之前是做Java开发,所以在这里增加一块两者的对比,有兴趣的可以看一看,JavaScript和Java在处理同步和异步操作方面确实有一些差异,主要体现在它们的运行时环境和编程模型上。
JavaScript中的同步和异步
JavaScript是单线程的,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript依赖于事件循环(Event Loop)机制。事件循环允许JavaScript在等待异步操作(如网络请求、定时器等)完成时继续执行其他任务,从而实现非阻塞的异步编程。
- 同步操作:在调用栈中按顺序执行,每个操作必须等待前一个操作完成。
- 异步操作:通过回调函数、Promise、
async/await
等机制实现,异步操作完成后会将回调函数放入任务队列,由事件循环调度执行。
Java中的同步和异步
Java是多线程的,支持并发编程。Java中的异步操作通常通过多线程实现,每个线程可以独立执行任务,从而实现并发和并行处理。Java也有丰富的并发工具,如ExecutorService
、Future
、CompletableFuture
等。
- 同步操作:在一个线程中按顺序执行,每个操作必须等待前一个操作完成。
- 异步操作:通过多线程、线程池、回调机制等实现,异步操作可以在不同的线程中并发执行。
Java中有没有事件循环?
严格来说,Java没有像JavaScript那样的内置事件循环机制。Java的异步操作主要依赖于多线程和并发工具,而不是事件循环。然而,在某些特定场景和框架中,Java也可以实现类似事件循环的机制。例如:
- Swing和JavaFX:Java的GUI框架(如Swing和JavaFX)使用事件调度线程(Event Dispatch Thread,EDT)处理用户界面事件。这类似于事件循环,但仅用于处理GUI事件。
- Netty:Netty是一个异步事件驱动的网络应用框架,它实现了自己的事件循环机制,用于处理网络I/O操作。
Java中的异步编程
让咱们看一个Java中的异步编程示例,使用CompletableFuture
来实现异步操作:
import java.util.concurrent.CompletableFuture;
public class AsyncExample {
public static void main(String[] args) {
System.out.println("Start");
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000); // 模拟异步操作
System.out.println("Async Task Completed");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
future.thenRun(() -> System.out.println("Callback Executed"));
System.out.println("End");
// 等待异步任务完成
future.join();
}
}
执行过程
-
开始执行:
System.out.println("Start");
输出 “Start”。 -
异步任务:使用
CompletableFuture.runAsync
启动一个异步任务,在另一个线程中执行。异步任务模拟执行1秒钟,然后输出 “Async Task Completed”。 -
注册回调:使用
thenRun
注册一个回调函数,当异步任务完成后执行,输出 “Callback Executed”。 -
继续执行:
System.out.println("End");
输出 “End”。 -
等待异步任务完成:使用
future.join()
等待异步任务完成,确保程序不会过早退出。
输出结果如下所示:
Start
End
Async Task Completed
Callback Executed
现在你对JavaScript中的同步和异步有了解了吗?不知道大家还有什么疑问?
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: