浏览器中的js事件循环(Event loop)
本文将简述浏览器中的js事件循环机制,帮助我们理解浏览器环境js代码是如何运行的。
Javascript的一大特点是单线程,也就意味着同一时间他只能做一件事。事件循环(Event Loop)是为了协调事件,用户交互,UI渲染,网络处理等行为,防止线程阻塞而诞生的。
浏览器事件循环
1.宏任务(Macro Task)和微任务(Micro Task)
浏览器中js事件循环的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。
常见的宏任务:定时器(setTimeout,setInterval)、script(整体代码)、 I/O 操作、UI渲染等。
常见的微任务: Promise.then(Promise的回调方法)、MutationObserver(html5新特性) 等。
2.事件循环过程解析
一个完整的事件循环过程可以概括为以下阶段:
① 刚开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。微任务队列为空,宏任务队列里有且只有一个 script 脚本(整体代码)。
② 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的宏任务与微任务,分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出宏任务队列,这个过程本质上是队列的宏任务的执行和出队的过程。
③ 上一步我们出队的是一个宏任务,这一步我们处理的是微任务。但需要注意的是:每执行完一个宏任务就会去检测微任务队列里是否有待处理的微任务,如果有,就会全部依次执行并清空队列;如果没有,就执行下个宏任务。
④ 执行渲染操作,更新界面
⑤ 检查是否存在 Web worker 任务,如果有,则对其进行处理
⑥ 上述过程循环往复,直到两个队列都清空
简单来说:
当一个宏任务执行完成之后,会检查是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中最靠前的一个,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,如此循环。
举个例子:
Promise.resolve().then(()=>{
console.log('Promise 1') ;
setTimeout(()=>{
console.log('setTimeout 1');
},0);
});
setTimeout(()=>{
console.log('setTimeout 2');
Promise.resolve().then(()=>{
console.log('Promise 2');
})
},0);
输出结果:Promise 1, setTimeout 2, promise 2, setTimeout 1
例子解析:
① 全局同步代码(整个代码块)执行(这属于宏任务),执行完毕(上述代码产生了一个宏任务setTimeout 2 和一个微任务Promise 1),检查并执行微任务队列,输出Promise 1,同时会生成一个宏任务 setTimeout 1。
② 然后去查看宏任务队列,宏任务 setTimeout 2 在 setTimeout 1 之前,先执行宏任务 setTimeout 2,输出 setTimeout 2。
③ 在执行宏任务setTimeout 2时会生成微任务Promise 2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出 Promise 2。
④ 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout 1。