浅析事件循环(Event Loop)

说到事件循环就不可避免的会谈到到任务队列,宏任务,微任务等等这些名词。那么问题来了,设计事件循环系统是为了解决什么问题,有了宏任务为什么还要有微任务??

单线程的JavaScript和多进程的浏览器

​ JavaScript这个语言在设计之初就是单线程,原因当然不是当初多核CPU还不够普及。作为主战场在浏览器的脚本语言,JavaScript的主要用途是与用户交互相关,以及操作DOM。这个场景决定了单线程比多线程更合适。比如同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程的操作结果为准?

​ 随着多核CPU的普及以及JavaScript的应用场景的转换,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以新标准并没有改变JavaScript单线程的本质。

​ 虽然JavaScript是单线程的,但是具有复杂功能的现代浏览器却是多线程的。

​ Chrome浏览器包括:1个浏览器(Browser)主进程、1个 GPU 进程、1个网络(NetWork)进程、多个渲染进程和多个插件进程

  • 主进程: 界面显示、用户交互、子进程管理,同时提供存储等功能
  • 插件进程:每个启动的插件都会创建一个进程
  • GPU进程:最初GPU是为了实现3D效果,后来Chrome选择采用GPU来绘制UI界面
  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 渲染进程:也是所谓的浏览器内核,核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。
任务队列和事件循环

​ 首先JavaScript的使用场景决定了它是单线程的。现在假定JavaScript引擎在执行script代码时,用户点击了页面上某个按钮,如果这个时候JavaScript引擎还在执行代码,那么只能由浏览器起一个线程来记录这个点击,等JavaScript引擎忙完了再来告诉它某个时候鼠标在某个坐标点击了一个按钮,需要你去执行按钮注册的回调函数。

​ 再进一步升级,如果有期间有多个事件发生了,那么需要一个队列来存储所有的事件,按照先来后到的顺序依次被执行。为了让这个事情更加的高效,事件循环系统就应运而生了,JavaScript引擎不断的从调用栈中取出任务执行,事件循环系统一次次的检查调用栈是否为空。如果调用栈为空,那么就会从任务队列中取出最早的一个任务来加入到调用栈中。

消息队列.png

宏任务和微任务

上面提到的任务队列就是宏任务队列,里面都是宏任务。可能看起来宏任务和事件循环已经可以满足需求了,那为什么还要有微任务和微任务队列。首先看看哪些任务属于宏任务哪些又属于微任务

  • 宏任务(macrotask)
    • 渲染事件(如解析 DOM、计算布局、绘制)
    • JavaScript 脚本执行事件
    • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
    • 网络请求完成(XMLHttpRequest)、文件读写完成(I/O)
    • 定时器任务(setTimeout,setInterval)
  • 微任务(microtask)
    • MutationObserver
    • Promise

宏任务可以满足大多数对时效要求不高的需求。一个最简单的例子setTimeout回调函数的执行时机问题,定时器触发线程只能保证在指定间隔时间之后,将回调函数加入到宏任务队列的队尾。因为每轮事件循环只有一个宏任务被执行,如果队列中已经有了很多任务,那么必定会影响后添加的宏任务的执行时机。所以类似setTimeout这些宏任务时间粒度比较粗,并不能精准的控制执行时机。

主流浏览器中目前还在用的产生微任务的两种方法。1,使用MutationObserver来观察DOM变化,当被监控的DOM属性,子节点,文本发生变化时,指定的回调函数便作为一个微任务添加到微任务队列。2,Promise.resolve()和Promise.reject()时,Promise的状态由pending变化为resolved或者rejectd之后,相应的回调函数也作为一个微任务添加到微任务队列。

一轮完整的事件循环

 <div>
 	<button id="firstBtn" style="background-color: green;">第一个按钮</button>
 	<button id="secondBtn">第二个按钮</button>
 </div>
<script>
    console.log("script start");

    // 第一个按钮监听点击
    document.querySelector("#firstBtn").addEventListener('click',()=>{
        console.log("firstBtn click");
        setTimeout(()=>{
            console.log("setTimeout1");
        },100)
        Promise.resolve().then(()=>{
            console.log('Promise3 callback');
        })
    })

    // 第二个按钮监听点击
    document.querySelector("#secondBtn").addEventListener('click',()=>{
        var el = document.querySelector("#secondBtn")
        console.log("secondBtn click");
        Promise.resolve().then(()=>{
            console.log('Promise4 callback');
        })

        setTimeout(()=>{
            console.log("setTimeout2");
        },0)
        for(let i=0;i<100;i++){
            el.style.color = i % 2 == 0 ? "blue" : "red";
        }
    })

    // 创建MutationObserver实例,监听第二个按钮DOM变化
    var observer = new MutationObserver((mutations,observer)=>{
        console.log("mutations callback");
    })

    observer.observe(document.querySelector("#secondBtn"),{
        childList:true,
        attributes:true,
    })

    console.log("script end");

</script>

1,在JavaScript引擎解析DOM并绘制页面之后,执行script脚本

2,控制台打印script start script end

3,如果点击了按钮1,按钮1的click回调函数添加到宏任务队列

4,调用栈为空,将回调函数从宏任务队列取出到调用栈中执行

5,控制台打印firstBtn click,Promise回调函数添加到微任务队列,定时器回调添加到宏任务队列

6,调用栈为空,依次执行微任务,直至清空微任务队列,控制台打印Promise3 callback,然后浏览器可以选择是否重新渲染页面

7,定时器触发线程经过100ms后触发,根据id将对应的定时器回调函数添加到宏任务队列中

8,调用栈为空,将定时器回调函数从宏任务队列取到调用栈中执行,控制台打印setTimeout1,宏任务和微任务队列都为空。

Snipaste_2020-04-26_16-35-46.png

9,如果此时点击了按钮2,同样会将按钮2点击事件回调函数加入到宏任务队列

10,控制台打印secondBtn click,Promise回调添加到微任务队列,定时器回调添加到宏任务队列,然后执行for循环中的DOM修改操作

11,100次for循环执行完,调用栈为空,MutationObserver因为监听到DOM修改,回调函数作为微任务添加到微任务队列

12,控制台打印Promise4 callbackmutations callback(仅打印一次),setTimeout2

这个打印顺序(mutations callback 在 setTimeout2之前)也验证了MutationObserver回调函数是个微任务。而且可以注意到for循环100次中每次都修改了DOM属性,但是MutationObserver仅触发了一次,也验证了MutationObserver回调函数是异步执行的,最后只统一执行一次,这也是MutationObserver 比MutationEvent性能更佳的原因

如果想让mutations callback打印100次该怎么做呢?

for(let i=0;i<100;i++){
    setTimeout(function(){
        el.style.color = i % 2 == 0 ? "blue" : "red";
    },0)
}

参考文档

JavaScript忍者秘籍(第2版)

浏览器工作原理

In The Loop Jake Archibald@JSconf 2018

Philip Roberts- What the heck is the event loop anyway? | JSConf EU 2014

posted @ 2020-04-26 21:38  SchneiderABB  阅读(460)  评论(0编辑  收藏  举报