本文深入解析 JavaScript 的单线程特性及事件循环机制。详细阐述执行栈、事件队列的工作原理,对比 setTimeout 与 setInterval 的行为差异,并重点剖析宏任务与微任务的执行顺序。通过 Promise 和 async/await 的示例代码,清晰展示异步编程中同步与异步任务的执行流程及输出顺序。
JavaScript 单线程特性与 Web Worker
一、关于javascript
JavaScript 被设计为单线程语言,尽管 HTML5 引入了 Web Worker 标准以支持多线程环境,但 JavaScript 核心引擎的单线程本质并未改变。
之所以采用单线程模型,主要源于其最初的设计用途:在浏览器中验证表单和处理 DOM 操作。若允许多线程,当多个线程同时对同一 DOM 节点进行冲突操作时,浏览器解析器将无法确定最终状态,导致解析失败。
Web Worker 的出现旨在为 JavaScript 创建多线程环境。它允许主线程创建 Worker 线程,将计算密集型任务分配给后台线程执行。主线程与 Worker 线程互不干扰,待 Worker 完成计算后,将结果返回给主线程。
JavaScript 事件循环机制
二、javascript事件循环
调用方法时,JavaScript 会生成对应的执行环境(即执行上下文)。该环境包含方法的私有作用域、上层作用域引用、参数、局部变量以及 this 对象。由于 JavaScript 是单线程的,同一时间只能执行一个方法,因此被调用的方法会被放入一个称为“执行栈”的队列中依次排队。
当 JavaScript 引擎遇到异步事件时,不会阻塞等待其返回结果,而是将其挂起,继续执行执行栈中的其他同步任务。
异步事件返回结果后,JavaScript 会将对应的回调函数放入一个与执行栈分离的队列,称为“事件队列”。放入事件队列的回调不会立即执行,而是等待当前执行栈中的所有任务执行完毕。当主线程处于空闲状态时,它会检查事件队列,若存在任务,则取出队首事件对应的回调放入执行栈,执行其同步代码。这一过程形成无限循环,即事件循环。
setTimeout 的执行行为
三、setTimeout
setTimeout 函数会在指定时间后,将待执行任务加入 Event Queue。由于 JavaScript 是单线程的,任务必须逐个执行,若前方任务耗时过长,后续任务只能等待。
setTimeout(() => {
task()
},3000)
sleep(10000000)
假设上述代码执行 task() 所需时间远超 3 秒,其执行过程如下:
- task() 进入 Event Table 并注册,计时开始。
- 执行 sleep 函数,该过程耗时极长,计时持续进行。
- 3 秒到期,计时事件 timeout 完成,task() 被移入 Event Queue。但由于 sleep 尚未执行完毕,task() 只能继续等待。
- sleep 终于执行完毕,task() 随后从 Event Queue 进入主线程执行。
setTimeout(fn,0) 的含义是指定某个任务在主线程最早可得的空闲时间执行。这意味着无需等待额外的秒数,只要主线程执行栈内的同步任务全部执行完毕且栈为空,该任务就会立即执行。
setInterval 的时间队列处理
四、setInterval
从执行顺序来看,setInterval 会每隔指定时间将注册的函数置入 Event Queue。若前方任务耗时过长,同样需要等待。
唯一需要注意的是,对于 setInterval(fn, ms),并非每过 ms 秒就会执行一次 fn,而是每过 ms 秒,fn 会被推入 Event Queue。
Promise 与 process.nextTick
五、Promise与process.nextTick(callback)
Promise 是异步编程的一种解决方案,相比传统的回调函数和事件机制,它更为合理且功能强大。Promise 本质上是一个容器,保存着未来才会结束的事件(通常为异步操作)的结果。从语法角度看,Promise 是一个对象,可用于获取异步操作的消息。它提供了统一的 API,使得各种异步操作可以采用相同的方式处理。
process.nextTick(callback) 类似于 Node.js 版的“setTimeout”,它会在事件循环的下一次循环中调用 callback 回调函数。
宏任务与微任务的执行顺序
六、宏任务和微任务
- macro-task(宏任务):包括整体代码 script、setTimeout、setInterval。
- micro-task(微任务):包括 Promise.then、process.nextTick。
不同类型的任务会进入对应的 Event Queue,例如 setTimeout 和 setInterval 会进入相同的 Event Queue。
事件循环的顺序决定了 JavaScript 代码的执行顺序。
进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到一个任务队列执行完毕,再执行所有的微任务。
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 这段代码作为宏任务,进入主线程。
- 先遇到 setTimeout,将其回调函数注册后分发到宏任务 Event Queue。
- 接下来遇到 Promise,new Promise 立即执行,因为 new Promise 回调函数中的代码是同步任务,then 函数被分发到微任务 Event Queue。
- 遇到 console.log(),立即执行。
- 整体代码 script 作为第一个宏任务执行结束,检查微任务。发现 then 在微任务 Event Queue 里面,执行它。
- 第一轮事件循环结束,开始第二轮循环,从宏任务 Event Queue 开始。发现宏任务 Event Queue 中 setTimeout 对应的回调函数,立即执行。
- 结束。
async/await 的执行流程
七、async await
1.async 做一件什么事情?
带有 async 关键字的函数,其返回值必定是一个 promise 对象。
也就是说,如果 async 关键字函数返回的不是 promise,会自动用 Promise.resolve() 进行包装。如果 async 关键字函数显式地返回 promise,则以返回的 promise 为准。
2.await 在等什么?
await 等待的是右侧「表达式」的结果。如果右侧是函数,则函数的 return 值即为「表达式的结果」。如果右侧是 'hello' 或其他值,则表达式的结果就是该值。
3.await 等到之后,做了一件什么事情?
await 右侧表达式的结果,就是 await 要等待的目标。等待之后,对于 await 来说,分为两种情况:
- 不是promise对象
- 是promise对象
如果结果不是 promise,await 会阻塞后续代码,先执行 async 外部的同步代码。同步代码执行完毕后,再回到 async 内部,将该非 promise 的值作为 await 表达式的结果。
如果等待的是一个 promise 对象,await 也会暂停 async 后续的代码,先执行 async 外部的同步代码。等待 Promise 对象 fulfilled 后,将 resolve 的参数作为 await 表达式的运算结果。
4.示例
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
- 这段代码作为宏任务,进入主线程。
- 先打印出 script start。接着执行函数 async1。
- 打印出 async1 start,执行到 await,执行函数 async2,打印出 async2。
- 此时 await 会阻塞 async1 后面的代码,先执行 async1 外部的同步代码。
- setTimeout 放入事件循环的宏任务。接着执行到 Promise,打印出 promise1,promise.then 放入事件循环的微任务。接着打印 script end。
- 现在 async1 外部的同步代码执行完毕,回到 async1 内部,打印出 async1 end。
- 整体代码执行完毕,执行微任务 promise.then,打印出 promise2。
- 最后执行宏任务 setTimeout,打印 setTimeout。
所以这段代码的执行顺序为:script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout
以上就是详解 JavaScript 的执行机制的详细内容,更多关于 JavaScript 执行机制的资料请关注本站其它相关文章!