JavaScript运行机制:Event Loop
注
:对阮一峰老师的JavaScript 运行机制详解:再谈Event Loop,做的学习笔记
1. 为什么JavaScript是单线程的
先来了解一下进程
和线程
这两个概念
比喻
进程
就是一个公司,每个公司都有自己的资源可以调度;公司之间是独立的;线程
就是公司里的每一个员工,多个员工一起合作,完成任务,公司可以有一名员工或多个,员工之间共享公司的空间
什么是进程
进程
:是cpu分配资源的最小单位;(是能拥有资源和独立运行的最小单位)
什么是线程
线程
:是cpu调度的最小单位;(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
举例
我们常用的浏览器就是多进程的,每打开一个tab页面,就会开启一个新的进程,这个进程里还有UI渲染线程,js引擎线程,http请求线程等,一个新的进程里通过开启多个线程来同步执行同一个任务的不同模块,从而高效完成这个任务。
为什么js不设计成多线程呢?
- 首先需要了解一下js的用途,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom
- 我们可以假设js是多线程的。此时他有两个线程在工作,一个线程要添加dom元素,而另外一个线程要删除这个dom元素。这个时候浏览器就很困惑了,到底要干神马捏?
- 所以为了避免这种复杂性,从一开始js就被设计成了单线程。
- 这是js的核心特征。
Web Worker
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质
2. 浏览器的渲染进程Renderer
概述
- 即通常所说的浏览器内核(Renderer进程,内部是多线程)
- 每个Tab页面都有一个渲染进程,互不影响
- 主要作用为页面渲染,脚本执行,事件处理等
- 渲染进程是多线程的
渲染进程的主要线程
1.GUI渲染线程
-
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
- 解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree
- 解析css,生成CSSOM(CSS规则树)
- 把DOM Tree 和CSSOM结合,生成Rendering Tree(渲染树)
-
当我们修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)
-
当我们修改元素的尺寸,页面就会回流(Reflow)
-
当页面需要Repaing和Reflow时GUI线程执行,绘制页面
-
回流(Reflow)比重绘(Repaint)的成本要高,我们要尽量避免Reflow和Repaint
-
GUI渲染线程与JS引擎线程是互斥的
- 当JS引擎执行时GUI线程会被挂起(相当于被冻结了)
- GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
2. JS引擎线程
-
JS引擎线程就是JS内核,负责处理Javascript脚本程序(例如V8引擎)
-
JS引擎线程负责解析Javascript脚本,运行代码
-
JS引擎一直等待着任务队列中任务的到来,然后加以处理
- 浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的
- 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
-
GUI渲染线程与JS引擎线程是互斥的,js引擎线程会阻塞GUI渲染线程
- 就是我们常遇到的JS执行时间过长,造成页面的渲染不连贯,导致页面渲染加载阻塞(就是加载慢)
- 例如浏览器渲染的时候遇到
<script>
标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况
3. 事件触发线程
-
属于浏览器而不是JS引擎,用来控制事件循环,并且管理着一个任务队列(task queue)
-
当js执行碰到事件绑定和一些异步操作(如setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
-
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
-
因为JS是单线程,所以这些待处理队列中的事件都得排队等待JS引擎处理
4. 定时触发器线程
-
setInterval
与setTimeout
所在线程 -
浏览器定时计数器并不是由JavaScript引擎计数的(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
-
通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待JS引擎空闲后执行),这个线程就是定时触发器线程,也叫定时器线程
-
W3C在HTML标准中规定,规定要求
setTimeout
中低于4ms的时间间隔算为4ms
5. 异步http请求线程
-
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
-
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中再由JavaScript引擎执行
-
简单说就是当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行
3. 同步任务和异步任务
-
因为js是单线程的,所以任务需要排队执行,前面的任务执行完毕了,才会执行后面的任务。如果前一个任务执行消耗了很长时间,后面的也得等着他。
-
有时候排队时间长,并不是因为CPU忙不过来了,CPU大部分时间都是闲着的。这是因为IO设备(输入输出)很慢(比如Ajax操作从网络读取数据),不得不等结果出来了,在往下执行
-
为了解决这个问题,后面任务执行顺序发生了改变:主线程抛开IO设备,挂起处于等待中的任务,线运行排在后面的任务,等IO设备返回结果了,在回头执行之前挂起的任务。被挂起的任务就是后面提出的异步任务。
-
由此,所有的任务被分成了两种:同步任务和异步任务
-
同步任务
- 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染
-
异步任务
-
不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
-
网页中加载图片音乐之类占用资源大耗时久的任务,就是异步任务
-
-
异步任务的执行机制
同步执行也是如此,因为它可以被视为没有异步任务的异步执行
- JavaScript有一个
main thread
主线程和一个call-stack
调用栈(执行栈),所有的任务都会被存放到调用栈中等待主线程执行- JS调用栈里装着一个个等待执行的函数,采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
- JS调用栈也是JS引擎的一部分
- 主线程之外,事件触发线程管理着一个
任务队列
(异步任务队列)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件回调。 - 所有同步任务都会直接在调用栈里排队等待执行,一旦调用栈中的所有同步任务执行完毕(也就是调用栈为空的时候),系统就会读取"任务队列",看看里面有哪些事件,那些对应的调用栈,当对应的事件被触发后就结束等待状态,进入执行栈,开始执行。
- 怎么知道调用栈是否为空呢?
- js引擎存在
monitoring process进程
,会持续不断的检查主线程调用栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数
- js引擎存在
- 怎么知道调用栈是否为空呢?
- 主线程不断重复上面的第三步。
示意图
只要执行栈空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复
4. 任务队列
“任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入调用栈了。主线程读取"任务队列”,就是读取里面有哪些事件。
任务队列分为:Event Table(事件表格)和 Event Queue(事件队列)
Event Table(事件表格)
-
Event Table 可以理解成一张
事件->回调函数
对应表 -
它就是用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表
Event Queue(事件队列)
-
Event Queue 简单理解就是
回调函数 队列
,所以它也叫 Callback Queue -
他里面存放了异步事件对应的回调函数
-
当 Event Table 中的事件被触发,事件对应的 回调函数 就会被 push 进这个 Event Queue,然后等待被执行
-
事件队列采用先进先出的结构,只要调用栈一清空,“任务队列”上第一位的事件就自动进入主线程
- 但是在遇到有定时器的异步事件时,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
5. Event Loop(事件循环)
event loop
即事件循环,是指浏览器或Node
的一种解决javaScript
单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。- Event Loop(事件循环)也是js的执行机制
来看一个流程图
- 程序开始运行后,任务进入调用栈(Call Stack)中
- 同步任务直接在调用栈中等待被执行,异步任务从调用栈移到Event Table注册
- 当Event Table中的异步任务事件被触发后(或延时到指定时间),Event Table会将事件对应的回调函数移入Event Queue 等待
- 当 调用栈中没有任务,就从 Event Queue 中拿出一个任务放入 Call Stack
上面这个过程会被不断的循环重复,这一整个循环就是Event Loop
它不停检查 Call Stack 中是否有任务(也叫栈帧)需要执行,如果没有,就检查 Event Queue,从中弹出一个任务,放入 Call Stack 中,如此往复循环。
6. 宏任务和微任务
上面介绍了任务可以分为同步任务和异步任务,其实细分起来还有宏任务和微任务
什么是宏任务
在ECMAScript中,macrotask
也被称为task
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他
由于JS引擎线程
和GUI渲染线程
是互斥的关系,浏览器为了能够使宏任务
和DOM任务
有序的进行,会在一个宏任务
执行结果后,在下一个宏任务
执行前,GUI渲染线程
开始工作,对页面进行渲染
宏任务 -> GUI渲染 -> 宏任务 -> ...
常见的宏任务
- 主代码块
- setTimeout
- setInterval
- setImmediate ()-Node
- requestAnimationFrame ()-浏览器
什么是微任务
ES6新引入了Promise标准,同时浏览器实现上多了一个microtask
微任务概念,在ECMAScript中,microtask
也被称为jobs
我们已经知道宏任务
结束后,会执行渲染,然后执行下一个宏任务
, 而微任务可以理解成在当前宏任务
执行后立即执行的任务
当一个宏任务
执行完,会在渲染前,将执行期间所产生的所有微任务
都执行完
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...
常见的微任务
- process.nextTick ()-Node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
宏任务、微任务的执行顺序
执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。