本文深入解析 JavaScript 单线程模型下的事件循环机制。通过剖析同步与异步代码的执行流程,详细阐述宏任务与微任务的优先级差异,并结合 setTimeout 与 Promise 示例,解释代码实际执行时间与预期延时不符的原因,帮助开发者掌握 JS 异步编程核心原理。
JavaScript 单线程模型概述
前言
JavaScript 采用单线程模型,即在任意时刻仅有一个主线程负责执行任务。这一设计源于其作为浏览器脚本语言的核心职责:与用户交互及操作 DOM。若允许多线程,将引发复杂的同步冲突,例如当两个线程同时对同一 DOM 节点进行添加和删除操作时,浏览器无法确定以哪个线程的操作为准。
尽管 HTML5 引入了 Web Worker 标准以利用多核 CPU 的计算能力,允许创建多个子线程,但子线程完全受主线程控制且禁止操作 DOM。因此,JavaScript 单线程的本质并未改变。
这是今天一个朋友发给我的一个面试题,
感觉还挺有意思的,
写个博客以供分享
先看看这个面试题目:
观察下面的代码,写出输出结果
console.log('0')
setTimeout(function () {
console.log('1');
});
new Promise(function(resolve,reject){
console.log('2')
resolve(3)
}).then(function(val){
console.log(val)
})
console.log(4)
输出结果: “0” “2” 4 3 “1”
今天主要是分析为什么输出结果这样的?这就和 javascript 的执行机制密切相关了.
Event Queue 与 Event Loop 机制
Event Queue 和 Event Loop
JavaScript 作为单线程语言,在执行代码时始终由唯一的主线程处理所有任务。
JavaScript 代码分为同步和异步两类,其处理方式存在显著差异。
- 同步任务直接进入主线程执行,而异步任务则进入 Event Table 并注册相应的回调函数。
- 当异步事件完成时,Event Table 会将对应的回调函数移入 Event Queue(事件队列)。
- 当主线程内的同步任务执行完毕且执行栈为空时,系统会读取 Event Queue 中的函数并将其移入主线程执行。
- 上述过程会不断重复, 也就是常说的 Event Loop(事件循环)
这里我们引进了 Event Queue 事件队列这一概念. 所有异步操作的回调都会进入到这里. 然后等到主线程空闲, 就会从这里调取回调执行.
setTimeout 的执行时序分析
setTimeout
setTimeout 相信大家都有使用过, 可以延时执行并且是异步执行的.
但是有时候我们得到的结果往往是代码实际执行的时间比我们想要延时执行的时间要久。这又是为什么呢?
这就和我们之前所说的 Event Loop 有关了, 我们可以来具体看下 setTimeout 的执行步骤:
setTimeout(function () {
asyncFn()
}, 1000);
syncFn()
- asyncFn 将异步执行函数放在 Event Table, 并且开始计时
- 开始执行 syncFn, 但是 syncFn 可能需要处理的内容很多, 执行时间超过 1 秒, 但是计时还在继续
- 计时到达 1 秒, setTimeout 延时完成, asyncFn 进入 Event Queue 事件队列, 但是主线程还在执行, 所以只能等待
- syncFn 执行完成, 此时 asyncFn 从事件队列中进入主线程执行
所以有时候会出现代码实际执行时间比延时时间长的情况。
宏任务与微任务的执行优先级
宏任务和微任务
之前我们说过异步任务会进入到事件队列中, 不同类型的任务会进入到不同的队列中, 比如宏任务会进入到宏任务队列中, 微任务会进入到微任务队列中.
我们只要记住 当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行
这时候我们就可以解释一开始的代码执行结果了:
- 主线程执行按顺序代码
- 遇到 setTimeout, 回调进入到宏任务队列上
- 遇到 Promise, 立即执行, then 函数进入到微任务队列
- 同步代码执行结束, 主线程检查是否存在微任务, 发现 then, 执行
- 微任务执行完毕, 再去查找宏任务 setTimeout, 执行
- setTimeout 执行结束, 检查是否存在微任务, 不存在, 结束.
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。