前置知识
进程线程
- 进程:CPU 在运行指令和保存上下文所需要的时间
打开微信,系统在执行打开指令,到加载微信上下文环境,直到彻底关闭微信前,都是一个进程
- 线程:是进程中的一个更小的单位,指的是执行一段指令所需的时间
打开微信聊天界面,就需要一个渲染线程, 同时获取到最新的消息,需要一个网络线程
小例子
浏览器新开一个不同域名的
tab页面,就是一个新的进程。其中有很多线程相互配合工作
这个过程涉及到的线程有:
http线程js引擎线程- 渲染线程
关于执行中的线程:
主线程:也就是js引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行
工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件(异步接下来咱们细细介绍)
所以:js引擎线程和渲染线程是互斥的,而通常其他线程之间通常是是可以同时工作的。这就导致只有js代码执行完毕后,渲染进程才会工作。会使在js代码量比较庞大时,会出现页面白屏。
异步
js是单线程的 ---v8在执行js代码时,默认只开启一个线程工作
为什么是单线程?
js天然可以操作DOM元素。若
js为多线程,想象一下:一个线程要删除某个DOM元素,另一个线程要修改它,那么到底应该以哪个线程的操作为准呢?为了避免这种情况发生,JavaScript从诞生之初就被设计为单线程
考虑到执行效率,v8会先执行同步代码,遇到异步代码时,会将异步代码存放到任务队列,等待 js引擎线程空闲时,再从任务队列中取出异步代码执行。
先来个代码感受一下:
let a = 1
setTimeout(() => {
a = 2
console.log(a)
}, 1000)
console.log(a)
运行过程:马上输出1,等一秒后输出2
事件循环-Event-Loop
JavaScript是单线程语言,这意味着它同时只能执行一个任务。为了处理异步操作,如网络请求、文件读写等,JavaScript使用事件循环机制。
JavaScript运行时包含以下几个主要部分:
- 调用栈(Call Stack) :这是代码执行的地方,所有的同步代码都会在这里执行。当调用一个函数时,它会被压入调用栈并开始执行。函数执行完毕后,它会被弹出调用栈。
- 事件队列(Event Queue) :异步操作完成后,相关的回调函数会被放入事件队列中等待执行。例如,当定时器(setTimeout)到时,其回调函数会被放入事件队列。
- 事件循环(Event Loop) :事件循环会监视调用栈和事件队列。如果调用栈为空,它会从事件队列中取出一个事件,并将其对应的回调函数放入调用栈中执行。这个过程会不断重复,形成事件循环。
js代码中有同步和异步之分,异步还被分为宏任务和微任务
微任务
promise.then, process.nextTick, MutationObserver
宏任务
setTimeout, setInterval, ajax, setImmediate, I/O, UI rendering
宏任务:有明确的异步任务需要执行和回调;需要其他异步线程支持
微任务:没有明确的异步任务需要执行,只有回调;不需要其他异步任务支持
执行顺序
- 先执行同步代码(这属于宏任务),这个过程中遇到异步,就分类存入任务队列
- 同步执行完毕后,先执行微任务队列中的代码
- 微任务全部执行完毕后,有需要的情况下渲染页面
- 渲染完毕后,执行宏任务队列中的代码 (开启了下一次的事件循环)
图解
代码案例
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('同步代码2')
resolve()
}).then(() => {
console.log('promise.then')
})
console.log('同步代码3');
最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"
解释:
- 首先,打印
同步代码1 - 将
setTimeout的回调函数放入宏任务队列 - 打印同步任务
new Promise中的同步代码2 - 将
new Promise的回调函数promise.then放入微任务队列 - 打印同步任务
同步代码3 - 执行微任务队列,打印
promise.then - 执行宏任务队列,打印
setTimeout
拓展: await
- 会将后续的代码挤入微任务队列
- 浏览器将 await 的执行时间提前了 (await 后面的代码当成同步来看待)
- 它会暂停
async函数的执行,直到await后面的Promise对象状态变为resolved或rejected
小测验
console.log('script start')
async function async1 () {
await async2()
console.log('async1 end')
}
async function async2 () {
console.log('async2 end')
}
async1()
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve, reject) => {
console.log('promise')
resolve()
}).then(() => {
console.log('then1')
}).then(() => {
console.log('then2')
})
console.log('script end')
输出结果:
"script start"、"promise"、"script end"、"async2 end"、"async1 end"、"then1"、"then2"、"setTimeout"
小结
掌握JavaScript的事件循环机制对于前端开发者来说至关重要。它不仅影响代码的执行顺序,还关系到性能优化和异步编程的能力。 通过清晰地解释事件循环的概念、宏任务和微任务的区别,以及提供具体的示例和解答常见问题,你将能够展示出自己在JavaScript异步编程方面的深厚功底。