谈谈自己对浏览器事件循环(Event Loop)的浅显理解

295 阅读4分钟

eventloop 又叫事件循环,指的是浏览器或者node在解决javascript单线程运行不会阻塞的一种机制。 在解释浏览器的事件循环之前先解释一下浏览器的运行机制。

一、浏览器多进程

首先,浏览器是多进程的,分为:

1.主进程(Browser 进程)

负责主控和协调、浏览器界面显示,与用户交互。如前进,后退等、负责各个页面的管理,创建和销毁其他进程、网络资源的管理,下载等;

2.第三方插件进程

每种类型的插件对应一个进程,仅当使用该插件时才创建;

3.GPU进程

只有一个,用于3D绘制,页面使用硬件加速才会用到他(比如看高清视频flash,显卡硬件加速)

4.浏览器渲染进程(浏览器内核)

也叫renderer进程,负责脚本执行、事件触发、队列的轮询等。打开多个浏览器,每一个tab浏览器都会启动一个渲染进程,内部是多线程的。

二、浏览器renderer进程是多线程的

1.JS引擎线程

也叫js内核,负责处理js脚本程序。一个渲染进程里只会有一个js线程解析js脚本,运行代码。(注意:GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞)

2.GUI 渲染线程

负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制。这里就涉及到回流和重绘,当页面需要重绘或者某些操作引起回流的时候,就会执行这个线程。当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。为什么会和js引擎线程互斥呢? 简单来说,因为js是可以操作dom的 如果你在执行js线程的同时执行UI线程,那就有可能导致渲染线程前后获得的元素不一样(或者说是js修改dom后没有重新渲染)

3.事件触发线程

对应事件触发时,该线程会将事件对应的回调函数放入callback中,等待js引擎线程处理

4.定时触发器线程

对应于setTimeout,setInterval API (由于js单线程,会阻塞,怕影响计时准确,通过单独线程来计时并触发定时) 计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行

5.异步 http 请求线程

在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行

三、事件循环 执行顺序

Javascript单线程任务被分为分为两种 同步任务 异步任务

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕 才能执行后一个任务
异步任务:不进入主线程 而进入任务队列的任务 只有任务队列通知主线程 某个任务队列可以执行 该任务才会进入主线程。异步任务必须指定回调函数 主线程执行异步任务 就是执行对应的回调函数)

在JavaScript中,任务被分为两种,一种宏任务 另外一种叫微任务

宏任务:script全部代码、setTimeout、setInterval、setImmediate等
微任务:Process.nextTick(Node)、Promise、MutationObserver

接下来看一个比较简单的事件循环的题目

 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((resolve) => {
   console.log("Promise");
   resolve();
 })
   .then(function () {
     console.log("promise1");
   })
   .then(function () {
     console.log("promise2");
   });

 console.log("script end");

自上而下执行的顺序 async/await 在底层转换成了 promise 和 then 回调函数。 也就是说,这是 promise 的语法糖

首先,打印script start,调用async1()时,返回一个Promise,所以打印出来async2 end。
每个 await,会新产生一个promise,但这个过程本身是异步的,所以该await后面不会立即调用。
继续执行同步代码,打印Promise和script end,将then函数放入微任务队列中等待执行。
同步执行完成之后,检查微任务队列是否为null,然后按照先入先出规则,依次执行。
然后先执行打印promise1,此时then的回调函数返回undefinde,此时又有then的链式调用,又放入微任务队列中,再次打印promise2。
再回到await的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,打印async1 end。
当微任务队列为空时,执行宏任务,打印setTimeout。
所以最后的输出就是:

script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout