Event loop事件循环

  1. 云栖社区>
  2. 博客>
  3. 正文

Event loop事件循环

廊桥梦醉 2018-10-11 11:55:00 浏览579
展开阅读全文

线程

javascript是单线程语言,也就是说同一个时间只能做一件事情,而这个单线程的特性与它的用途相关,作为浏览器脚本语言,javascript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题,比如,假定javascript同时又两个线程,一个线程在某个结点上添加内容,另一个线程删除了这个结点,这个时候浏览器应该以哪个线程为准?

为了利用多核CPU的计算能力。HTML5提出了web Worker标准,允许javascript脚本创建多个线程,但是子线程完全受到主线程的控制,且不得操作DOM。所以,这个新标准并没有改变javascript单线程的本质。

【排队】

单线程就意味着所有任务需要排队,前一个任务接收,才能执行后一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着

img_64bcb18e7a903cd536e0024fd1a35137.png

像上面这样,如果排队是因为计算量大CPU忙不过来倒也算了,但是,如果是网络请求就不合适了,因为一个网络请求的资源什么时候返回时不可预知的,这种情况再排队等待就不明智了。

同步和异步

于是任务分为同步任务和异步任务

【同步】

如果在函数返回的时候,调用者就能够得到预期结果(即拿到预期的返回值或者看到预期的效果),那么这个函数就是同步的

img_6a47e18588aee33a9d174da9b0b46d5d.png

第一个函数返回时,就拿到了预期的返回值:2的平方根;第二个韩式返回的时候,就看到了预期的效果:在控制台打印了一个字符串。

所以这两个函数都是同步的

【异步】

如果在函数返回的时候,调用者还不能够得到预期的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。

img_28d1ea736fb82afd3c38c9fbbe6995ee.png

在上面的代码中,我们希望通过fs.readFile函数读取文件foo.txt中的内容,并打印出来。但是在fs.readFile函数返回的时,我们期望的结果并不会发生,而是要等到文件全部读取完成之后,如果文件很大的话,可能要很长时间。

所以fs。readFile函数是异步的

正式由于javascript是单线程的,而异步容易实现非堵塞,所以在javascript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。

异步详解

从上文可以看出,异步函数实际上很快就调用完成了,但是后面还有执行一步操作、通知主线程、主线程调用毁掉函数等很多步骤。我们把整个过程叫做异步过程。异步函数的调用在整个异步过程中,只是一个小部分。

一个异步过程通常是这样的:主线程发起一个异步请求,异步任务接受请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时异步操作开始执行,执行完成后通知主线程,主线程收到通知后,执行一定的动作(调用回调函数)

因此,一个异步过程包括两个要素:注册函数和回调函数,其中注册函数是用来发起异步过程的,回调函数用来处理结果。

下面代码中,其中setTimeout就是异步过程的发起函数,fn是回调函数

img_f797b1b64cb878c3b619700366050caa.png

有一个很重要的问题,如何才算是异步操作执行完成呢,对于不同类型的异步任务,操作完成的标准不同。

【异步类型】

一般而言,异步任务有以下三种类型

1.普通事件:如click、resize等等

2、资源加载:如load、error等

3、定时器:setInterval、setTimeout等

下面对这三种类型分别举例说明,下面代码中,鼠标点击div时,就代表任务执行完成了。

img_da4894dc16956470c035432e70bb8889.png

下面代码中,XHR对象的readyState值为4,即已经接收到全部响应数据了,代表任务执行完成

img_b4671d8037535c3f062a38ca014e1903.png

下面代码中,过1s后,代表任务执行完成

img_18c89f3be66971ebfd417eef5a941720.png

对于同步任务来说,按照循序执行即可,但是们对于异步任务,各任务执行的时间长短不同,执行完成的时间点也不同,主线程如何调控异步任务呢?这就用到了消息队列

【消息队列】

有些文章把消息队列成为任务队列,或者叫事件队列,总之是和异步任务相关的队列

可以确定的是,它是队列这种先入先出的数据结构,和队列类似的,哪个异步操作完成的早,就排在前面。不论异步操作何时开始执行,只要异步操作执行完成,就可以带消息队列中排队

这样,主线程在空闲的时候,就可以从消息队列中获取消息并执行

消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现相关。但是为了简单起见,可以认为:消息就是注册异步任务时,添加的回调函数。

可视化描述

人们把javascript调控同步和异步任务的机制成为事件循环,首先来看事件循环机制的可视化描述

img_994c20cb4f27fd12cd44cbf83fc1c076.png

【栈】

函数调用行程一个栈帧

img_2b654ffa77a690d1a596d982e94e8410.png

当调用bar的时,创建了第一个帧,帧中包含了bar的参数和局部变量。当bar调用foo的时候,第二帧就被创建,并被压到第一帧之上,帧中包含了foo的参数和局部变量。当foo返回时,最上层的帧就被弹出栈(剩下的bar函数的调用栈)。当bar返回的时候,栈就空了。

【堆】

对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域

【队列】

一个javascript运行的时候包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈拥有足够的内存的时候,从队列中取出赖一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。

事件循环

下面详细介绍事件循环,下图中,主线程运行的时候,产生堆和栈,栈中的代码调用各种外部的API,异步操作执行完成后,就在消息队列中排队。只要栈中的代码执行完毕,主线程就会去读取消息队列,依次执行那些异步任务所对应的回调函数

img_b8c73c02ce0013ba4adca3e6a3d6f4aa.png

详细步骤如下:

1、所有同步任务都在主线程上执行,形成一个执行栈

2、主线程之外,还存在一个“消息队列”。只要异步操作执行完成,就到消息队列中排队。

3、一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取消息队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行

4、主线程不断的重复上面的第三步。

【循环】

从代码执行循序的角度来看,程序最开始是按代码顺序执行代码的,遇到同步任务立即执行;遇到异步任务则只是调用异步函数发起异步请求。此时,此时异步任务开始执行异步操作,执行完成之后到异步消息队列中排队。程序按照代码顺序执行完毕之后,查询消息队列中是否有等待的消息。如果有则按照次序从消息队列中把消息放到执行栈中执行,执行完毕之后,再从消息队列中获取消息,再执行,不断重复。

由于主线程不断的重复获得消息、执行消息、再取消息、再执行。所以,这种机制被称之为事件循环

用代码表示大概是这样

img_2b41be363e3177d0b43e9e3c2baaea16.png

如果当前没有任何消息queue.waitForMessage会等待同步消息到达。

【事件】

为什么叫事件循环?而不叫任务循环或者消息循环。究其原因是消息队列中的每条消息实际上都是对应一个事件

DOM操作对应的是DOM事件,资源加载操作对应的是加载事件,而定时器操作可以看做对应一个‘时间到了’的事件

实例

下面以一个实例来解释事件循环机制

img_7a3219e04d3932843b2760af5e3eb6a7.png

1、执行第一行代码,第一行是一个同步任务,控制台显示1;

2、执行第二行,第二行是一个异步任务,发起异步请求,可以在任意时刻执行鼠标点击的异步操作

3、执行第三行代码,第三行是一个同步任务,控制台显示2

4、执行第四行代码,第四行是一个异步任务,发起异步请求,1s后执行定时器任务

5、假设熊执行第四行代码的1s内,执行了鼠标点击,则鼠标任务在消息队列中排到首位

6、从执行第四行代码1s后,定时器任务到消息队列中排到第二位

7、现在同步任务已经执行完毕,则从消息队列中按照次序把异步任务放到执行栈中执行

8、则控制台依次显示click、timeout

9、过了一段时间后又执行了一次鼠标点击,由于消息队列中已经空了,则鼠标任务在消息队列中排到首位

10、同步任务执行完毕后,再从消息队列中按照次序把异步任务放到执行栈中执行

11、则控制台显示click

【异步过程】

下面以一个实例来解释一次完整的异步过程

img_5b66bb738d798e8a71c08bd172e083f3.png

1、主线程通过调用异步函数div.onclick发起异步请求

2、在某一时刻执行异步操作,即鼠标点击

3、接着,回调函数fn到消息队列中排队

4、主线程从消息队列中读取fn到执行栈中

5、然后在执行栈中执行fn里面的代码console.log('click')

6、于是。控制台显示click

同步变异步

每一个消息完整的执行后,其他消息才会被执行。这点提供了一些优秀的特性,包括每当一个函数执行时,它就不能被强占,并且在其他代码运行之前完全运行

这个模型的一个缺点在于当一个消息需要太长时间才能完成,web应用无法处理用户的交互,例如点击和滚动

于是,对于这种情况的常见优化是同步变异步

一个例子是创建webQQ的QQ好友列表,列表中通常会有成百上千个好友,如果一个好友用一个节点来表示,在页面中渲染这个列表的时候,可能要一次性往页面中创建成百上千个节点。

在短时间内往页面中大量添加DOM节点显然也会让浏览器吃不消,看到的结果往往就是浏览器卡顿甚至假死,代码如下:

img_5543b065b40387abf32ef45b866e5be6.png

这个问题解决方案之一就是数组分块技术。下面的timeChunk函数让创建节点的工作分批进行,如果把1s创建1000个节点,改为每隔200ms创建8个节点

img_063078f2822c7355f29afff807e5c58a.png
img_98d8e513e1f56dbf8ad4d81bf385481b.png

数组分块的重要性在于它可以将多个项目的处理在消息队列上分开,在每个项目处理之后,给予其他的异步任务的执行机会,这样就可能避免长时间运行脚本的错误。一旦某个函数需要50ms以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务。

当我们打开网站时,网页的渲染过程就是一大堆同步任务,像页面骨架和页面元素的渲染,而加载图片、音乐之类的任务就是异步任务,看一下下边导图:

img_3d46d488a2eb9d00b7bdb5891b461e29.png

如图:

同步和异步任务分别进入不同的执行“场所”,同步进入主线程,异步进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,回去了Event Queue读取对应的函数,进入主线程。

上述过程会不断重复,也就是常说的Event Loop(事件循环)。

任务队列的宏任务和微任务

JS异步还有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入event queue,然后再执行微任务,将微任务放入event queue,但是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回调函数,然后再从宏任务的queue拿宏任务的回调函数,如下图:

img_f1dd717f84f11509e51f9231d90a9363.png

事件循环只有一个,但任务队列可能有多个,任务队列可分为宏任务和微任务。XHR回调、事件回调(鼠标键盘事件)、setImmediate、setTimeout、setInterval、indexedDB数据库操作等I/O/以及UI rendering都属于宏任务(也有文章说UI render不属于宏任务,目前还没有定论),process.nextTick、Promise.then、Object.observer(已经被废弃)、MutationObserver(html5新特性)属于微任务。

网友评论

登录后评论
0/500
评论
廊桥梦醉
+ 关注