Event loop 是如何工作的

138 阅读8分钟

来源于Jake Archibald于2018年在jsconf上的分享,虽然时间有点远,但是是一次深度好分享!twitter.com/jaffathecak…

如果你想了解下面的内容,那请一定要阅读本篇文章哦~

🎥 事件循环是如何工作的?

➡️ 其他线程的东西如何进入JS主线程的呢?

➡️ 为什么无限循环会阻塞渲染?

➡️ 为什么 setTimeout 循环不会阻塞渲染?

➡️ rAF 与 setTimeout对比

➡️ rAF 是在paint之前还是之后?

➡️ 微任务何时发生

抛砖引玉

当你看到这样的代码时,会不会有疑惑🤔,用户会不会先看到元素被渲染出来,然后再隐藏呢?

虽然实际运行中,上面这种情况没有出现过,但是你为了确保没问题,会交换两行代码的位置

那到底会不会出现“元素闪现的问题”呢?这就要从浏览器的event loop说起了

event loop

在网页的运行中,有一个控制器叫做main thread(图中黄色的代表主线程),它处理着许多事情,比如rendering,执行javascript,操作dom等等。为了保证页面的正常运行,主线程会按一定的顺序依次处理这些事件, 不会同一时间执行着这个,又执行着那个,不会同时又不同的任务操作着dom等,这样会出现可怕的竞争关系,造成运行状态杂乱无章的后果。

按顺序依次执行,就会带来有的任务执行时间过长,阻碍后续的任务执行的问题,有可能阻碍rendering或是用户的交互。

那如何帮助主线程减负,别让它太操劳——浏览器多线程机制来解救,这里出现了多个线程(比如,网络线程、GPU线程)完成不同的工作,等它们完成工作后需要把结果告知主线程,以便持续推进。能保证主线程和其他线程和谐工作的机制就是

下面想象一下这个场景,你有一个setTimout(callback, ms)函数,当主线程调用它的时候,需要等待500ms后才能继续向下执行,这将会阻碍许多任务的执行,这样的效果并不是浏览器和用户想要的,那怎么改变一下呢?开一个新线程执行setTimout,这样可以让主线程继续执行后续代码,达到同时执行的效果。那当500ms后,setTimout执行完毕,需要在合适的时间告知主线程结果,防止出现主线程和新线程同时操作同一个dom,从而导致混乱状态,这样就需要一个队列存储新开线程的结果,待合适的时间告知主线程执行。

形象化event loop,如下图所示,它一直随着cpu的运行状态转圈圈

当task queue被加进来后,event loop可以被形象化成下图这样,当browser告知event loop,我有一个task让你做,event loop把这个task加到自己的todo list中,在自己合适的时间执行它

当要event loop要执行两个setTimout时:

当render task也被加进来时,event loop如何处理呢?

S->style, L->layout, P->painting

当browser告诉event loop,我要更新视图了,event loop将会执行style,layout,painting步骤。

那如果event loop一直在中心的圈里运行(js执行infinite loop),不去执行右半部分的逻辑,那视图就会卡住。

在点击执行循环按钮之前,这个小猫咪gif会动,同时你也可以选中文字,但是在执行死循环后,gif停止了,也不能选中文字了。

当用户click这个button时,browser告诉event loop说我现在有一个task让你处理,event loop选择合适的时间去处理这个task,然后这个task的执行不会结束,event loop就卡那里了。即使随后browser告诉event loop,现在你要去更新视图了,even loop也只能回复说“好的,我现在很忙,等我执行完这个infinite loop后,就立马执行视图更新”,再随后用户click别的地方,新的task被加了进来,也没法执行,因为event loop必须要等待infinite loop执行完毕后(这就出现了render blocking),再继续。

从上面的图,可以看出,event loop会保证task被执行完后,再去执行下次rendering, 那回到开头的问题,当执行这段代码(先append dom,再hiden它)时,browser会闪烁一下吗?答案就是不会,因为event loop会先把js执行完毕后,再去更新视图。

下面的代码,也是一个无限循环,但是是setTimeout的方式持续推进,那这会阻碍试图渲染吗?答案是不会

event loop正常执行主线程逻辑,当执行到loop方法时,开启新线程执行它,同时主线程继续执行,当新线程的逻辑执行完,会把结果存入task queue,在合适时机,event loop开始从task queue中拿出这个task,开始执行,随后又开始执行主线程逻辑,上面的过程又重复一遍......, 然后当browser告诉event loop你该更新视图了,event loop开始执行rendering(记住event loop会保证task被执行完后,再去执行下次rendering

当你想执行一些和rendering相关的任务,使用task并不不明智,因为下次rendering process一定会在task执行完毕后才执行,从上面的图也可以看出,task的执行和rendering的执行正好在event loop的两边,形容一下就是执行中会“顾此失彼”。那使用什么呢?requestAnimationFrame

requestAnimationFrame

requestAnimationFrame在处理css之前执行,在rendering之前执行

执行下面两段代码,会发现setTimeout(虽然延迟0ms,但是会在大约4.7ms后执行)的box比requestAnimationFrame的bix移动的更快,这说明settimeout被调了更多次。

那event loop在多次执行task后,才去执行requestAnimationFrame(browser决定何时去执行它,大多数屏幕刷新频率在1s更新60次),说明setTimeout执行频率超过了屏幕正常的刷新频率

让我们设想下图所展示的每一块,代表着用户看到的frame

render在这些frame开头发生,比如style calculation,layout,paint等,可以看到是有序的一种状态

当task加入时,就呈现一种随机的,杂乱的状态,那视图在task中被修改,那有可能在更新不及时的情况

setTimeout(延迟0ms)时,会发现每frame至少有3次tasktask是不会影响渲染的,所以很多动画库会这样设置setTimeout(callback, 1000/60)

当task执行过程,有可能block rendering,如下所示

当改为requestAnimationFrame,frame将会变得整洁起来,有顺序起来,如下所示

当然frame中除了requestAnimationFrame,还会有其他task在执行,比如点击事件等


下面的代码,看起来操作dom频繁,但其实并不会执行这么多次rendering,因为这些js task会在rendering之前就执行完毕

看下面的代码,你会发现效果会是box从0变为500,没有从0变为1000,再变为500的过程

哪怕这样也不行,why?

下面解释一下,当用户click button后,发生如下变化

之后才开始计算style,layout和paint,所以你要想实现效果,需要两个requestAnimationFrame

或者使用比较hack的做法,这会让browser 提前强制更新dom

microtasks

Back to 1990,w3c提供了监听dom操作的事件DOMNodeInserted 上面执行这个span loop,DOMNodeInserted会被触发200次(span append 100次+textcontent设置为hello 100次,当然如果有冒泡,还会触发更多次),即使开发者想要监听到变化发生,但实在没必要监听到这么多次,实际开发中,只想知道“变化了”这个状态就行了,这时候微任务mutationObserver就出现了。

下面的代码,会让gif停止动,用户也选中不了text了,不像settimeout loop那样,这是为什么呢?这和微任务的执行机制有关,微任务在主线程js执行完后执行,在同步代码之后执行,而且会被本次循环中的所有微任务都执行完后,再开启后续任务的执行

下面画了task,requestAnimationFrame和microtasks的执行机制,由于演示是个视频,这里我就截取了视频中的关键帧贴了出来。

task的执行机制是执行一个,新加进来的task会被推入队列,后续event loop执行

requestAnimationFrame的执行机制是会把本次事件循环中的执行完毕,后续加进来的,下次rendering前执行

microtasks执行机制是把本次的任务执行完,当还有新任务加进来时,也会把新的执行完毕后,再把执行权交给别人,所以可能会阻碍rendering


当用户click button,执行这段代码,会发生什么呢?

......

那执行这段代码,又会发生什么呢? 这次换代码中自动click button

平时写代码时要注意automated执行这种click事件,因为它们的执行结果会和用户真实执行不一致

这时候是不是觉得该执行微任务了,结果并不是,因为这时候js stack还不是空的,button.click has not returned yet,所以要执行listener2

可以正常阻止default:

不能正常阻止default: When you execute the click() method, It's too late to call prevent default in a microtask, so you miss the time taht you can actually cancel that event

分享就到此结束了,如有不对欢迎指正~ 欢迎留言讨论~