JS执行机制、同步和异步、宏观任务和微观任务

1. JS 是单线程

  • JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事
  • 这是因为 Javascript 这门脚本语言诞生的使命所致——JavaScript 是为处理页面中用户的交互,以及操作 DOM 而诞生的。
  • 比如我们对某个 DOM 元素进行添加和删除操作,不能同时进行。 应该先进行添加,之后再删除。
	单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
	这样所导致的问题是: 如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

问题

以下代码执行的结果是什么?

 console.log(1);   // 先打印1
 
 setTimeout(function () {
     console.log(3); // 1s钟之后打印3
 }, 1000);
 console.log(2);  
// 如果是单线程,这个代码只有在打印3之后,才打印2。 时间比较久,需要等待上个定时器执行完成
// 如果是多线程,就会打印1,直接打印2,再去打印3 (实际打印:1,2,3)

那么单线程,多线程是什么呢?继续往下走:

2. 同步任务和异步任务

​ 单线程导致的问题就是后面的任务等待前面任务完成,如果前面任务很耗时(比如读取网络数据),后面任务不得不一直等待!!

​ 为了解决这个问题,利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制。于是,JS 中出现了同步任务异步任务

同步

​ 前一个任务结束后再执行后一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的。比如做饭的同步做法:我们要烧水煮饭,等水开了(10分钟之后),再去切菜,炒菜。

异步

​ 你在做一件事情时,因为这件事情会花费很长时间,在做这件事的同时,你还可以去处理其他事情。比如做饭的异步做法,我们在烧水的同时,利用这10分钟,去切菜,炒菜。
在这里插入图片描述

JS中所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务指的是:
	在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是:
	不进入主线程、而进入”任务队列”的任务,当主线程中的任务运行完了,才会从”任务队列”取出异步任务放入主线程执行。

理解

同步,不是两件事情同时去做,而是先做一件事情,然后再做另一件事情。他是单线程,一个人依次做多件事情

异步,是两件事情同时去做,他是多线程,多个人同时做多个事情

同步任务,异步任务

再来看一个问题

console.log(1);

setTimeout(function() {

	console.log(3);

}, 0);
console.log(2);
// 结果还是1,2,3
// 为啥呢?继续往下走:

我们来看一下同步任务和异步任务:
在这里插入图片描述
了解完这一块之后,我们知道了刚刚的问题代码中,setTimeout的回调函数,是异步任务,放到任务队列了,但是为啥后执行呢?继续往下走:

3. JS执行机制

3.1 执行顺序
  1. 先执行执行栈中的同步任务。
  2. 异步任务(回调函数)放入任务队列中。(但是不执行回调函数
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行。

如图:
在这里插入图片描述

3.2 事件循环

问题

console.log(1);
document.onclick = function () {
    console.log('click');
}
console.log(2);
setTimeout(function () {
    console.log(3)
}, 3000)

分析过程:

  1. 此代码中有两个异步任务,我们先给同步代码和异步代码分开:
    在这里插入图片描述

  2. 当主线代码执行完了之后,1,2打印完了。

  3. 3s之后,定时任务时间到了,就将fn放到异步任务的任务队列中了:
    在这里插入图片描述

  4. 然后执行异步任务的打印3。最终出现1,2,3。

  5. 如果setTimeout的回调函数的任务执行完成,异步任务队列就为空了。

  6. 如果此时点击了document,onclick的回调函数就放到异步任务了
    在这里插入图片描述

  7. 然后执行,最终出现1,2,3,click

  8. 执行完了,异步任务又会清空

  9. 最后有个注意事项,主线程代码执行完成之后,会反复去异步任务中看是否有任务需要执行
    在这里插入图片描述
    由于主线程不断的重复获得任务、执行任务、再获取任务、再执行,所以这种机制被称为事件循环( event loop)

事件循环综合图:
在这里插入图片描述

3.3 代码思考题
 console.log(1);
 document.onclick = function() {
   console.log('click');
 }

 setTimeout(function() {
   console.log(3)
 }, 3000)
 console.log(2);

4.宏观任务和微观任务

4.1 宏观任务:

宿主发起的任务为宏观任务,如script(整体代码)、setTimeout、setInterval、setImmediate、I/O、ajax、UI交互事件、postMessage、MessageChannel

4.2 微观任务:

JavaScript引擎发起的任务为微观任务,如Promise.then、MutationObserver、process.nextTick(Node.js 环境)的意思就是定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行

如何分析异步执行的顺序

  • 首先我们分析有多少个宏任务;

  • 在每个宏任务中,分析有多少个微任务;

  • 根据调用次序,确定宏任务中的微任务执行次序;

  • 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;

  • 确定整个顺序;

如下代码解析:

<script type="text/javascript">
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})`
</script>

第一个宏观任务:

  1. 第一个宏观任务script 作为整体进入主线程 遇到console.log(‘1’) 输出1
  2. 遇到setTimeout,宏观任务(现在第一个宏观任务script还没有执行完毕 会分配到宏观任务中 暂时还不会执行)
  3. 遇到下面的process.nextTick 分配到微观任务中
  4. 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中 5.此时的微观任务表中:process.nextTick,Promise.then 则执行结果为 6 8
    第一个宏观任务 执行结果 : 1 7 6 8

第二个宏观任务:

  1. 第二个宏观任务是第一个setTimeout
  2. 跟script的执行顺序是一样的 碰到console.log(‘2’) 执行2 process.nextTick会分配到微观任务 Promise会立即执行 然后.then分配到微观任务
    输出结果:2 4 3 5

第三个宏观任务:

第三个宏观任务就是第二个setTimeout 和第二个宏观任务执行顺序一样
输出结果 9 11 10 12

<think>好的,用户现在需要我详细展开讲解回调、异步任务事件驱动这三个概念。首先,我需要明确每个概念的定义、实现方式、适用场景以及它们之间的区别联系。用户可能对这些异步编程中的关键机制不太清楚,所以需要从基础入手,结合例子说明。 首先,回调(Callback)。回调是一种函数传递机制,调用方将函数作为参数传递给被调用方,当被调用方完成任务后,调用该函数反馈结果。需要区分同步回调异步回调,重点讲异步回调的应用场景,比如JavaScript中的事件处理,或者Java中的CompletableFuture回调。 然后是异步任务(Async Task)。异步任务指的是在后台线程执行任务,调用方无需等待结果。需要介绍如何启动异步任务,比如Java的Thread、ExecutorService,或者更高级的FutureCompletableFuture。要说明异步任务如何与主线程解耦,以及如何获取结果(如Future.get()或CompletableFuture的then方法)。 接下来是事件驱动(Event-Driven)。事件驱动是通过事件来触发程序流程的模式,核心是事件发布-订阅机制。需要讲解事件源、事件对象、事件监听器(订阅者)的角色,以及如何通过事件循环处理事件。例子可以用GUI程序的按钮点击事件,或者Node.js的事件循环。 还要比较这三者的区别联系。比如,回调是一种具体的实现方式,异步任务任务执行的模式,而事件驱动是一种架构模式。它们都用于处理异步操作,但侧重点不同。 需要注意用户可能的疑问点,比如回调地狱的问题,异步任务的异常处理,事件驱动的性能优化等。在讲解时要结合实际例子,让概念更具体。 最后,确保回答结构清晰,分点讲解每个概念,然后总结对比。同时,按照用户要求,在末尾生成3-5个相关问题。 </think> 回调、异步任务、事件驱动是异步编程中解决“非阻塞执行”的三大核心机制,分别从**结果反馈方式**、**任务执行模式**、**系统交互架构**三个维度提供异步解决方案。以下结合定义、实现方式、适用场景及对比展开讲解。 ### 一、回调(Callback):结果反馈的“函数传递”机制 回调是一种**通过函数(或方法)传递实现结果通知**的模式。调用方将一个函数(回调函数)作为参数传递给被调用方,被调用方完成任务后主动调用该函数,将结果反馈给调用方。 #### 核心特点 - **解耦调用与执行**:调用方无需阻塞等待,被调用方通过回调函数“反向调用”通知结果。 - **同步/异步均可**:回调本身是中性的,既可以用于同步场景(如遍历数组时的处理函数),也可用于异步场景(如异步I/O完成后的通知)。 #### 异步回调的典型实现 以Java的`CompletableFuture`为例(异步I/O场景): ```java // 启动异步任务(读取文件) CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { return new String(Files.readAllBytes(Paths.get("data.txt"))); } catch (IOException e) { throw new RuntimeException(e); } }); // 注册回调:文件读取完成后执行 future.thenAccept(content -> { System.out.println("文件内容:" + content); }).exceptionally(e -> { // 异常回调 System.err.println("读取失败:" + e.getMessage()); return null; }); // 主线程继续执行其他任务(非阻塞) System.out.println("主线程处理其他操作..."); ``` #### 常见问题:回调地狱(Callback Hell) 当多个异步操作需要链式依赖时(如A完成后执行B,B完成后执行C),嵌套的回调会导致代码层级过深,可读性维护性下降: ```javascript // JavaScript 示例(回调地狱) asyncTaskA((resultA) => { asyncTaskB(resultA, (resultB) => { asyncTaskC(resultB, (resultC) => { // 最终处理resultC }); }); }); ``` **解决方案**:通过Promise(JavaScript)、`CompletableFuture`(Java)等“链式调用”机制扁平化回调结构: ```javascript // JavaScript Promise 优化 asyncTaskA() .then(resultA => asyncTaskB(resultA)) .then(resultB => asyncTaskC(resultB)) .then(resultC => { /* 处理resultC */ }); ``` ### 二、异步任务(Async Task):后台执行的“独立单元” 异步任务是指**在后台线程或线程池中执行任务**,调用方启动任务后无需等待其完成,可继续执行其他操作。任务完成后,通过结果句柄(如`Future`)或回调获取结果。 #### 核心组件 - **任务提交者**:发起任务的线程(通常是主线程)。 - **任务执行者**:后台线程或线程池(如Java的`ExecutorService`)。 - **结果句柄**:用于跟踪任务状态(如`Future`接口,提供`isDone()`判断完成状态,`get()`阻塞获取结果)。 #### 典型实现:Java的`Future`与`CompletableFuture` 1. **基础异步任务(`Future`)** 通过`ExecutorService`提交任务,返回`Future`对象: ```java ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(() -> { Thread.sleep(1000); // 模拟耗时操作 return 42; }); // 主线程非阻塞,可执行其他操作... System.out.println("主线程继续运行..."); // 最后获取结果(若任务未完成,get()会阻塞) int result = future.get(); // 阻塞等待1秒后返回42 ``` 2. **增强型异步任务(`CompletableFuture`)** 支持**链式调用**、**组合多个任务**(如`thenCombine`)、**异常处理**等高级特性,解决`Future`的阻塞获取结果问题: ```java CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 10); CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> 20); // 合并两个任务的结果(task1task2完成后执行) CompletableFuture<Integer> combined = task1.thenCombine(task2, (a, b) -> a + b); combined.thenAccept(sum -> System.out.println("总:" + sum)); // 输出30 ``` #### 适用场景 - I/O密集型任务(如数据库查询、网络请求),避免主线程阻塞。 - 计算密集型任务(如复杂算法),通过多线程并行加速。 ### 三、事件驱动(Event-Driven):系统交互的“发布-订阅”模式 事件驱动是一种**通过事件触发程序流程**的架构模式。核心是: - **事件源**:产生事件的对象(如按钮点击、传感器数据更新)。 - **事件监听器(订阅者)**:注册对特定事件的兴趣,事件触发时执行处理逻辑。 - **事件循环(Event Loop)**:负责收集事件、分发给对应监听器处理(单线程或多线程)。 #### 典型流程 1. **注册监听器**:订阅者向事件源注册监听器(如`addClickListener(handler)`)。 2. **触发事件**:事件源在特定条件下(如用户点击按钮)生成事件对象。 3. **分发事件**:事件循环将事件分发给所有注册的监听器。 4. **处理事件**:监听器执行回调逻辑(如更新UI、记录日志)。 #### 典型实现:Node.js的事件循环与GUI程序 1. **Node.js(单线程事件驱动)** Node.js通过单线程的事件循环处理异步I/O,避免多线程开销: ```javascript const fs = require('fs'); const eventEmitter = new (require('events').EventEmitter)(); // 注册监听器:文件读取完成事件 eventEmitter.on('fileRead', (content) => { console.log("读取到内容:" + content); }); // 异步读取文件(触发事件) fs.readFile('data.txt', 'utf8', (err, content) => { if (err) throw err; eventEmitter.emit('fileRead', content); // 触发事件 }); // 主线程继续执行(非阻塞) console.log("等待文件读取..."); ``` 2. **GUI程序(如Swing)** 按钮点击事件的处理是典型的事件驱动: ```java JButton button = new JButton("点击我"); button.addActionListener(e -> { // 注册监听器(回调) JOptionPane.showMessageDialog(null, "按钮被点击!"); }); ``` #### 核心优势 - **高扩展性**:新增事件类型或监听器无需修改现有代码(符合开闭原则)。 - **低耦合**:事件源与监听器仅通过事件对象通信,无直接依赖。 ### 三者对比与关联 | 维度 | 回调(Callback) | 异步任务(Async Task) | 事件驱动(Event-Driven) | |---------------------|------------------------------------------|-----------------------------------------|-----------------------------------------| | **核心目标** | 结果反馈(如何通知调用方) | 任务执行(如何非阻塞执行) | 系统交互(如何协调多组件) | | **实现粒度** | 函数/方法级(微观) | 任务级(中观) | 系统架构级(宏观) | | **依赖关系** | 调用方与被调用方直接关联(回调函数传递) | 调用方通过结果句柄(如Future)关联 | 事件源与监听器通过事件间接关联 | | **典型问题** | 回调地狱(嵌套过深) | 结果获取阻塞(需结合回调或轮询) | 事件顺序混乱(需处理事件优先级) | | **关联** | 异步任务常通过回调反馈结果 | 事件驱动中事件处理可能触发异步任务 | 事件触发逻辑可通过回调实现 | ### 总结 - 回调是异步通知的“基础工具”,但需注意避免嵌套过深。 - 异步任务是“执行单元”,解决任务非阻塞执行问题,结合`CompletableFuture`可优化结果处理。 - 事件驱动是“架构模式”,通过解耦事件生产与消费,提升系统灵活性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈善强

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值