前言
之前在阅读Js书籍时,就看见过事件循环的概念。最近又阅读了相关的文章以及setTimeout(fn, 0)背后所涉及的知识点,所以结合以前阅读的文章以及最近学习到的相关知识点,总结下Event Loop相关知识。
JavaScript线程
浏览器中至少存在三个线程:
- JavaScript引擎:基于事件驱动的单线程
- GUI渲染:与Js引擎互斥,当页面需要重绘或回流时执行
- 事件触发:将事件处理程序添加到任务队列中,等待Js引擎解释执行
我们常说的浏览器内核实际上指的是渲染引擎,负责UI的渲染的,当你通过DOM改变页面节点的样式时,页面就会重绘,该引擎就会工作。
我们知道JavaScript是单线程的,这里所说的JavaScript是单线程的就是指Js引擎线程,它是所有Js代码执行的地方,而Js引擎内部就是基于事件驱动的。
Js引擎内部中事件驱动是怎样的实现原理呢?简单地说,
实际上内部维持着一个队列。这个队列被称为任务队列,该队列中存储着所有被触发的异步处理程序,引擎通过循环形式将异步程序程序入栈处理
while(true) {
// tick
}
每一轮循环称为tick,当事件被触发,事件处理程序就会被加到队列的队尾,因为是单线程,所以需要等待队列中其他程序执行完毕才可以执行该事件程序。
Js事件循环(Event Loop)实际上就是如此实现的:
当事件被触发,处理程序加入到任务队列中等待Js引擎的执行,所有的任务都是排队等待被执行。
我们知道Js是解释型语言(实际上js存在编译),代码是从上到下依次执行的,对于未来执行的代码称为异步代码,相对的不异步执行的就是同步代码。
简单的说,Js引擎在执行同步异步代码的过程大概如此:
- 所有的同步任务在主线程上执行,形成执行栈。
- 主线程之外,存在任务队列,只要异步任务有了结果,就会在任务队列中放置相关的任务,等待同步任务被执行完后,由引擎循环入栈异步程序开始执行。
实际上, Js引擎内部优先执行同步代码,同步代码不执行完是不会执行异步代码的,即不会将任务队列中的异步代码入栈执行。
下面看一个实例,就明白了上面的意思:
结果:
我们可以看出,输出b的定时器是在for循环之后才输出的,为什么会如此?
因为Js引擎中同步代码没有执行完,引擎不会循环将任务队列中的异步事件处理程序入栈处理
那么此处setTimeout(fn, 0)表示何意呢?难道不是立即执行吗?实际上说立即执行不是很准确,准确的描述是:
当同步代码执行完毕后,立即执行该定时器中异步代码
我们已经知道Js单线程以及内部Event Loop的大概执行思路,那就明白了异步事件执行以及定时器都是存在精度问题,它们精度都要取决于异步代码被入栈执行的时间,而入栈时间取决于同步代码执行的快慢。
总结
- Js引擎内部Event Loop机制用于处理异步处理程序
- 任务队列存储所有被触发的异步程序
- Js引擎处理完所有的同步代码之后,才会通过Event Loop机制获取任务队列中队列头部的程序,并入栈进行处理
- Event Loop机制内部实现简单点来说就是一个无限循环