浏览器基本原理初探(二)—— EventLoop
在说浏览器事件循环之前,我们先思考几个问题:
单线程是如何处理异步操作的?
到底什么是事件循环?
task(宏任务)和microtask(微任务)是什么?
单线程和异步
为啥是单线程?
现在的浏览器都是多进程架构,以chrome为例,包含浏览器进程,渲染进程,网络进程...等,之前的笔记有提过,这么不再赘述。那么,为什么又说浏览器是单线程呢?其实这里的单线程指的是浏览器渲染页面的主线程,html解析,css计算,js代码执行,layout,绘制等都由主线程完成,也就是说其中任何一个任务执行超时都会阻塞后面的任务执行。
同步代码和异步任务
JavaScript的任务分为同步任务和异步任务,同步任务在主线程解释执行JavaScript代码时依次进入执行栈执行,异步任务被添加到异步任务队列,并添加异步标记等待执行
执行栈和任务调度
同步任务依次进入执行栈,并把函数参数,上下文环境等存储在一个称之为变量对象的数据结构中。函数执行完成后,变量对象从执行栈弹出。
继续把下一个同步任务压入执行栈执行,直到所有同步任务执行完成
事件循环
同步任务执行完成后,主线程开始遍历异步任务队列,并根据异步任务标记判断该任务是否可以执行。如setTimeout定时任务时间到了,把callback加入到执行栈开始执行。依次完成一轮异步任务队列遍历执行,这种主线程轮询异步任务队列执行的过程就是事件循环。在一个执行帧(60FPS大约是16.6ms)内可能发生多次事件循环,具体取决于浏览器刷新率和异步任务时间复杂度
task(宏任务)和microtask(微任务)
事件循环我们搞清楚了,那么,宏任务和微任务又是什么呢?其实,异步任务队列就是一个宏任务队列,微任务队列是基于宏任务执行过程中产生的任务队列。常见的任务区分如下
宏任务(task):JavaScript主程序,定时任务(setTimeout,setInterval),I/O操作,UI render等;
微任务(microtask):promise回调(.then和.cache),queueMicrotask(草案),Object.observe,以及nodejs中的MutationObserver,process.nextTick等
主线程调度单个宏任务执行流程:
首先执行宏任务主体代码;
宏任务执行过中产生微任务,把微任务添加到微任务队列中;
宏任务主体代码执行完成后,遍历执行产生的微任务;
如果微任务执行过程中又产生了微任务,继续添加到微任务队列;
直到宏任务产生的微任务全部执行完成,即清空微任务队列;
继续事件循环执行下一个可执行的宏任务...
微任务队列执行采用了队列先进先出的规则,但微任务队列并不是一个真的队列结构,而是一个set。此处参考html文档
看过很多文章说微任务优先于宏任务执行,其实是不准确的。就单个宏任务而言,主体代码肯定是优先于微任务执行的,因为微任务的产生依赖于宏任务主体执行,没有父哪来子,你说呢?从宏任务队列的角度出发,上一个宏任务产生的微任务肯定是优先于下一个宏任务执行的,这是JavaScript代码依次执行的规则。
好了,事件循环,宏任务,微任务我们都搞清楚了,下面来验证一把
//主程序
console.log('script start')
//遇到宏任务,加入到宏任务队列
setTimeout(function () {
console.log('script timeout running')
}, 0)
//ps:promise执行代码也属于主程序代码
const promise = new Promise((resolve, reject) => {
//微任务执行过程中产生的宏任务,加入到宏任务队列
console.log('promise running')
setTimeout(() => {
console.log('promise timeout running')
}, 0)
resolve()
})
//遇到微任务,加入到微任务队列
promise.then(() => {
console.log('promise then1 running')
//微任务执行过程中产生的微任务,加入到微任务队列
Promise.resolve().then(() => {
console.log('promise then resolve running')
})
})
//遇到微任务,加入到微任务队列
Promise.resolve().then(function () {
//微任务执行过程中产生的微任务,加入到微任务队列
queueMicrotask(() => console.log('queueMicrotask running'))
console.log('promise then2 running')
})
console.log('script end')
//主程序执行结束
输出结果