1. 浏览器中的事件循环
当我们书写的javascript代码,通过浏览器进行加载到渲染页面,再到页面交互,清楚我们代码中的执行顺序谁先谁后对我们来说尤为重要.
这部分知识就涉及到浏览器的事件循环
1. 进程和线程
- 进程: 我们这样理解,计算机运行的程序 项目 应用 都可以看成一个进程,运行着我们所启用的应用。当应用关闭也意味着,进程在我们计算机中退出. 在明显一点,我们的任务管理器/活动监视器每一个条目就是一个进程。
- 线程:进程中又包含了线程 一个进程中最少有一个或多个线程,线程是计算机调用的最小单位。
- 我们在打开浏览器页卡都是一个进程,这些进程中包含了很多的线程:
- js引擎线程 解析和执行js代码 也称为主线程
- 事件触发线程 事件触发Dom事件 发布订阅事件 js回调等
- 定时器触发线程 专门处理定时器的 定时器触发 定时器回收
- 异步Http请求线程 处理资源中的http请求的
- GUI渲染 在资源请求到后 会进行页面资源的渲染, 发生css合并 Dom树的渲染 Dom的回流和重绘等
- 这些线程之间的相互配合,渲染出我们浏览器的页面显示和交互效果
2. 异步和同步
- 同步: 代码自上而下执行,上面代码没有执行完毕,下面的代码不会被执行
- 异步: 异步是相对同步而言的。上面代码执行过程中不会 阻塞代码的执行。
- 像在浏览器的渲染过程中js主线程执行栈会执行同步代码和异步代码 同步代码按照顺序自伤而下开始执行, 遇到异步代码 当前的主线程会吊起另一个线程 定时器就开启定时器线程 http请求就开启http请求线程...。 定时器webAPI在触发等待事件会将当前的定时器函数存储在内存中的红黑树中,在定时器时间到达的时候取出来 压入当前的主线程执行栈中 开始执行定时器中的代码。
- 在当前的执行任务中 如果当前执行栈中同步代码发生了阻塞,那么后续的异步执行代码将不会被执行 除非等待当前的同步任务执行完毕 才会按照原异步任务加入队列的顺序开始进行下一个异步任务,直到异步任务执行完毕后,事件循环执行完毕。
// 同步代码不执行完毕。 异步代码不会被执行 setTimeout(() => { // 因为死循环 异步代码不会被执行 document.body.style.background = 'red' }, 0); while(true) { }
宏任务和微任务
- 宏任务是有宿主环境提供的
- 微任务是有语言本身进行提供的
- 在上一个所说的等待加入执行栈的任务其实是一个宏任务。用于做异步回调的逻辑执行。
1. 常见的微任务:Promise.then() MutationObserver queueMicrotask node中的process.nextTick()
2. 常见的宏任务: 事件 http请求 定时器 ui渲染 setImmidiate requestAnimationFrame等
事件循环调用栈图
-
- 事件循环和事件循环多列是有事件线程进行维护的,在执行栈中每次执行的都是宏任务的代码.
-
- 当前的宏任务代码执行微任务和宏任务,分别添加到宏任务和微任务队列的队尾,待当前宏任务中的同步代码执行完毕以后,会首先清空微任务队列.
-
- 然后在进行GUI页面渲染,待渲染完毕后会在宏任务中取出待执行的宏任务压入执行栈,继续当前的宏任务操作
常见面试题
document.body.style.background = 'red'
console.log(1)
Promise.resolve().then(() => {
console.log(2)
document.body.style.background = 'yellow'
})
console.log(3)
黄色还是红色到黄色的过渡?
- 会一直显示是黄色:因为微任务的代码实在GUI渲染之前执行。事件环调用栈图可见
btn.addEventListener('click', function() {
console.log('listener1')
Promise.resolve().then(() =>{
console.log('micro task1')
})
})
btn.addEventListener('click', function() {
console.log('listener2')
Promise.resolve().then(() =>{
console.log('micro task2')
})
})
btn.click()
- btn.click()执行时js调用栈在执行的时候js线程会对addEventListener会合并执行,不牵涉到宏任务,所以输出时listener1 listener2 micro task1 micro task2
- 当我们注释 btn.click()进行点击按钮触发 由于浏览器中点击事件时宏任务,所以会牵涉到事件循环队列,这时候输出就是 listener1 micro task1 listener2 micro task2
console.log(1)
async function async1() {
console.log(2)
await console.log(3)
console.log(4)
}
setTimeout(() => {
console.log(5)
}, 0);
const promise = new Promise((resolve) => {
console.log(6)
resolve(7)
})
promise.then(res => {
console.log(res)
})
async1()
console.log(8)
- await的之后的执行过程可以看成时
Promise.resolve()包裹,await语句后续执行逻辑是放在Promise中的then微任务中。 - 所以
await console.log(3)相当于Promise.resolve(console.log(3)).then(() =>{})的操作.console.log函数会立即执行. - 输出结果:
1 6 2 3 8 7 4 5