前言
在当今的Web开发中,JavaScript扮演着核心角色。它不仅驱动着网页的交互,更是实现复杂应用的关键。然而,JavaScript的单线程特性带来了挑战:如何在不阻塞用户界面的情况下处理大量异步操作?这就引出了事件循环机制------JavaScript的心脏,它通过协调任务的执行,确保了程序的流畅运行。本文将深入探讨这一机制,揭示其背后的原理和工作方式。
正文
同步代码
同步代码 等待任务完成才能继续执行下一行代码。也就是说在同步操作完成之前,程序会暂停执行,不会进行任何其他操作。通常适用于那些执行时间非常短的任务,比如简单的数学运算 及本地变量赋值等。
示例代码
javascript
console.log('同步代码开始');
let sum = 5 + 3; // 同步操作,立即得到结果
console.log('5 + 3 的结果是', sum);
console.log('同步代码结束');
异步代码
异步代码允许程序在等待某些操作完成时继续执行其他任务。
异步代码是需要耗时的代码,其中的任务队列又分为微任务 和宏任务
微任务
微任务(Microtasks)是JavaScript中用于快速响应异步事件的轻量级任务,它们在当前宏任务完成后立即执行,优先于下一个宏任务。主要包括:
Promise.then()
: 用于注册Promise
解决或拒绝时的回调函数,这些回调作为微任务加入队列。process.nextTick()
: Node.js特有的方法,允许在当前操作完成后立即执行回调,作为微任务。MutationObserver
: 监听DOM变化的Web API,当检测到变化时,其回调函数作为微任务执行。
宏任务
宏任务(Macrotasks)是JavaScript事件循环中的大任务单元,它们通常表示一些较大的操作或延时执行的任务,主要包括:
script
: 浏览器加载和解析JavaScript脚本文件,整个脚本作为一个宏任务执行。setTimeout
: 设置一个定时器,当指定时间到达后,其回调函数作为一个宏任务被加入队列。setInterval
: 与setTimeout
类似,但会周期性地重复执行回调函数,每次执行都是一个宏任务。setImmediate
: 在Node.js中,用于在当前事件循环结束后立即执行回调函数。在浏览器中,可以通过setTimeout(callback, 0)
实现相似效果。- I/U: 包括文件读写、网络请求等,完成后的回调函数作为宏任务处理。
- UI-rendering:浏览器自动更新页面元素的视觉表示,以反映最新的DOM和样式变更的过程。
示例代码
javascript
console.log('异步代码开始');
setTimeout(() => {
console.log('这是异步操作,不会阻塞主线程');
}, 1000);
console.log('异步代码结束');
尽管setTimeout
函数设置了1秒的延迟,但程序不会等待这1秒结束,而是会立即执行下一行代码,即打印"异步代码结束",然后继续执行其他任务。1秒后,setTimeout
中的回调函数会被执行:
进程和线程
- 进程:CPU运行指令和保存上下文所需的时间
- 线程:执行一段指令需要的时间
比如:一个浏览器的tab页面
- 渲染线程
- js引擎线程
- http线程
js的加载是会阻塞页面渲染的,即渲染线程和js引擎线程是不能同时工作的
多线程
- 定义:任务分散到多个线程上并行执行。
- 优点:能更好地利用多核处理器,提高计算效率。
- 缺点:增加了编程复杂性,需要管理线程间的同步。
- 应用:适合CPU密集型任务,如视频编码、3D渲染等
单线程
- 定义:所有任务在同一个线程上按顺序执行。
- 优点:简单,避免了并发问题,易于开发和调试。
- 缺点:长时间运行的任务可能阻塞后续任务执行。
- 应用:适合I/O密集型任务,如JavaScript在浏览器中的执行。
js的单线程
v8在执行js的过程中,只有一个线程会工作
- 节约性能
- 节约上下文切换的时间
Event-Loop步骤
- 执行同步代码(这属于是宏任务)
- 同步执行完毕以后,检查是否有异步需要执行
- 执行所有的微任务
- 微任务执行完毕以后,如果有需要就会渲染页面
- 执行异步宏任务,也是开启下一次事件循环
我们来看下面的示例
javascript
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
分析
(为了方便理解,笔者这里会用一些"代号"代替某段代码)
我们按照Event-Loop的步骤进行逐步解释:
console.log(1)
是同步任务,直接输出1
;new Promise()
是一个函数的调用,并不是微任务里的Promise.then()
,所以它其实是一个同步的代码。执行输出2
;.then()3
这是一个微任务,要进微任务队列(这里用then代替),之后直接看setTimeOut()5
;
setTimeOut()5
,看到是一个定时器,直接放入宏任务队列(这里用set1代替);
-
console.log(7)
输出7
,到这里Event-Loop的第一步就结束了; -
检查是否有异步代码要执行,先看微任务队列,有
.then(console.log(3))
,先直接输出3
; -
.then
里的setTimeout()4
,是宏任务,放入宏任务队列(这里用set2 代替),此时.then
已经执行完了,出微任务队列,第一次的事件也已经执行完了,现在开始下一次宏任务; -
宏任务队列中现在是有
setTimeout()5
和setTimeout()4
,本着队列先进先出的原则,先执行set1 ,里面有console.log(5)
直接输出5
; -
发现
setTimeout()5
里还有一个setTimeout()6
,这个是第二次事件循环发现的,啥也别说了,你也进宏队列(set3代替);
- 我们此时的微任务队列都已经执行完了,就一个
.then()
嘛,可以直接看宏任务队列 - 看set2 ,对应的是
setTimeOut()4
这段代码,里面的console.log(4)
直接执行输出4
,set2执行完毕,出队。第二次事件循环结束,开启下一个宏任务。 - 没有微任务,执行set3 ,是
setTimeOut()6
这段,执行console.log(6)
输出6
,结束! 看运行结果:
async
async
是 JavaScript 中的一个关键字,用于声明一个异步函数
- 声明方式 :使用
async
关键字声明的函数会在其内部自动返回一个Promise
对象。 - 返回值 :如果函数正常执行完毕,
Promise
会被解决(resolved)并返回函数的返回值;如果函数中抛出错误,Promise
会被拒绝(rejected)。 - 错误处理 :使用
try...catch
语句在异步函数内部捕获错误,防止Promise
被隐式拒绝。 await
关键字 :在async
函数内部,可以使用await
关键字等待一个Promise
解决。await
只能在async
函数内部使用。
示例:
javascript
// 声明一个异步函数
async function fetchData() {
try {
// 使用 await 等待一个 Promise 解决
const data = await fetch('https://api.example.com/data');
const result = await data.json();
console.log(result);
} catch (error) {
console.error('An error occurred:', error);
}
}
// 调用异步函数
fetchData();
在这个示例中,fetchData
是一个异步函数,它使用 await
等待 fetch
请求完成并解析 JSON 数据。如果在等待过程中发生错误,错误会被 catch
块捕获。
使用 async
和 await
可以简化异步代码的编写,使代码看起来更像是同步的,同时保持了异步执行的优势。
代码理解
javascript
console.log('script start');
async function async1() {
await async2()
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1()
setTimeout(function () {
console.log('setTimeout');
}, 0)
new Promise(function (resolve, reject) {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
})
console.log('script end');
第一遍:
第二遍:
运行结果:
结语
以上就是本篇文章的全部内容,JS的事件循环机制是有点绕,但是根据示例代码来进行逐步分析就能很好地理解,希望本篇文章对读者有所帮助,感谢阅读!