前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解事件循环

深入理解事件循环

作者头像
Chor
发布2019-11-07 20:50:56
7650
发布2019-11-07 20:50:56
举报
文章被收录于专栏:前端之旅前端之旅

本篇博客讲的东西偏底层,较难理解。虽然有的地方不够精准和全面,但是我觉得对于理解js中的异步来说已经够了,所以没有再深究一些概念(比如浏览器在这个过程中充当的角色)。

1.单线程

所谓的单线程,可以简单理解为做事情讲究先来后到,要做后面的事情,你得等前面的事情做完—–不管它需要多久。 既然如此,js引擎为何还要采取这种单线程的机制呢? js主要是与用户互动,这个过程涉及到对DOM节点的操作,如果js是多线程的,一个在节点上添加内容,一个要对这个dom节点进行删除,到底是以哪个为准?所以这就是为什么js从一出现就秉承着单线程的运行机制。 另外还要注意:

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

2.同步任务和异步任务

很显然,单线程会带来一个问题:就是代码执行的阻塞。比如:排在前面的任务如果耗时长,则后面的任务不得不一直等待它。 如果说耗时长是因为计算量大、cpu一直忙着计算的话倒也还好,可事实是——大部分时间浪费在了IO上(ajax从网络上获取数据),还有其他的如鼠标点击、setTimeout等等。因此这里提出了同步任务和异步任务的概念。

在js中,可以将同步和异步简单理解为执行顺序的问题。

2.1同步(sync):

即上面所说的后面等待前面。同步对应了同步任务(synchronous),即可以按照正常顺序执行的任务,比如加载页面骨架等。

2.2异步(async):

即把耗时长的任务挂起,先执行耗时短的,再回过头执行耗时长的。 异步对应了异步任务(asynchronous),即不适合按照正常顺序执行的任务,主要包括:

  • onclick等事件绑定—> 当事件触发时,回调函数会被添加到任务队列中;
  • setTimeout / setInterval 等计时器—> (时间延迟)当浏览器完成计时,回调函数会被添加到任务队列中;
  • AJAX请求—>当网络请求完成返回时,回调函数会被添加到任务队列中

3.事件循环

  • 事件循环又叫event loop,需要注意的是,事件循环不是单线程的js引擎提供的机制,而是来自于js引擎的运行环境(多线程的浏览器或node.js)。
  • 事件循环是实现异步的一种机制。一个线程中只有一个事件循环,我们将这个循环的每一次循环执行过程称之为tick。 具体每一次循环是怎么执行的,后文会讲。

4.执行栈和任务队列

事件循环机制离不开执行栈和任务队列的相互配合。js中将同步任务放到主线程上执行,形成“执行栈”;异步任务则放到任务队列中。

任务队列的分类标准之一:

一个线程可以拥有多个任务队列。每一个任务队列都对应某一任务源,并包含了一堆来自该任务源的任务。任务源是什么?像setTimeout/Promise/DOM事件/AJAX等都是任务源,来自同类任务源的任务我们称它们是同源的,比如setTimeout与setInterval就是同源的。

任务队列的分类标准之二:

在ES6中,我们用另一种方式对任务队列进行分类。 宏任务: 即macro-task,包括整体代码script,setTimeout,setInterval、AJAX、用户I\O 等。宏任务会对应地进入宏任务队列中; 微任务: 即micro-task,包括Promise,process.nextTick(callback)(可以理解为node.js版的setTimeOut)。微任务会对应地进入微任务队列中。

5.事件循环的具体实现过程?

总的来说,事件循环的顺序,决定了js代码执行的顺序。

  • 首先进入<script>包裹的整体代码(这是第一个宏任务),标志着第一次循环开始。在整体代码的执行过程中,同步任务照旧执行,异步任务分发到对应的任务队列中;
  • 整体代码执行完,执行栈清空,开始读取任务队列;
  • 读取所有微观任务队列 -> 执行 -> 第一次循环结束,开始第二次循环 读取一个宏观任务队列 -> 执行 -> 读取所有微观任务队列 -> 执行 -> 第二次循环结束,开始第三次循环 读取一个宏观任务队列…………….. ……… ……… 队列清空,执行栈清空,事件循环正式结束。

PS:读取任务时,会执行这些任务指定的回调函数,并且要注意:若回调函数中又有宏任务,则该宏任务会被安排到下一轮循环中。

6.事件循环的例子

下面通过三个由易到难的例子来理解上面所说的过程。

例1

代码语言:javascript
复制
setTimeout(() => {
task()
},3000)

sleep(10000000)

分析: 跑一下代码,会发现控制台执行task()需要的时间远远超过3秒,这就说明我们有的人理解的”setTimeout的第二个参数指定了多长时间后执行回调函数”的说法是错误的。 让我们来分析一下这个过程:

  • <script>中的整段代码作为第一个宏任务,进入主线程。即开启第一次事件循环;
  • 首先遇到了setTimeout,将其回调函数task()进入Event Table并注册,同时浏览器开始计时;
  • 继续,遇到了sleep函数,这是同步任务,所以直接执行。但是速度很慢,非常慢,而浏览器计时仍在继续;
  • 好了,3秒终于到了,计时事件setTimeout总算完成,可以把task()放入任务队列了;
  • 但是主线程上的sleep太慢了,还没执行完,于是我们只好等着;
  • sleep终于执行完了,执行栈清空,第一次循环的宏任务结束;
  • 读取微任务队列….不对,没有任何任务被分发到这个队列,于是第一次循环只好这样结束了;
  • 第二次循环开始,读取宏任务队列,刚好,里面有一个setTimeout对应的task()回调函数,压栈、令其进入主线程执行;
  • 执行栈清空了,任务队列也清空了,事件循环正式结束。

现在,我们知道setTimeout的回调函数是一开始就注册进event table的,但是那时并未进入任务队列—-要经过一定的时间,而这个时间由第二个参数来指定。也就是说,第二个参数指定的是“多长时间后将回调函数放入到任务队列中”。 另外,即使回调函数已经进入队列,也得先等主线程的执行栈清空后才有可能轮到自己。 我们还经常遇到setTimeout(fn,0)(或者干脆没有指定第二个参数)这样的代码,这是不是意味着可以立即执行呢? 不是。setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是注册进event table的同时就将任务放入队列中,只要主线程执行栈内的同步任务全部执行完成,且此时没有微任务队列,那么该任务就会马上压栈并执行。

例2

代码语言:javascript
复制
setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})
console.log('console');

分析:

  • <script>中的整段代码作为第一个宏任务,进入主线程。即开启第一次事件循环;
  • 遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务队列中(第二个参数不设定时,默认延迟为0);
  • 接下来遇到new Promise、Promise,立即执行,输出: promise 。将then函数分发到微任务队列中;
  • 遇到console.log,立即执行,输出: console
  • 整体代码作为第一个宏任务执行结束,此时去微任务队列中查看有哪些微任务,结果发现了then函数,然后将它推入主线程并执行,输出: then
  • 第一轮事件循环结束,第二轮事件循环开始;
  • 先从宏任务开始,去宏任务队列中查看有哪些宏任务,结果发现了setTimeout对应的回调函数,将它推入主线程并执行,输出:setTimeout
  • 然后去微任务队列中查看是否有事件,结果没有;
  • 此时第二轮事件循环结束;
  • 执行栈清空了,任务队列也清空了,事件循环正式结束。

例3

代码语言:javascript
复制
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

分析:

第一轮事件循环:

代码语言:javascript
复制
a) 整段`<script>`代码作为第一个宏任务进入主线程,即开启第一轮事件循环
b) 遇到console.log,立即执行。输出:1
c) 遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务队列中。
我们将其标记为setTimeout1
d) 遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务
队列中。记为process1
e) 遇到new Promise、Promise,立即执行;then回调函数放入Event table中注册,然后
被分发到微任务队列中。记为then1。
输出: 7
f) 遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务队列中。
我们将其标记为setTimeout2

此时第一轮事件循环宏任务结束,下表是第一轮事件循环宏任务结束时各任务队列的情况

可以看到第一轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行

代码语言:javascript
复制
g)、执行process1。输出:6
h)、执行then1。输出:8

第一轮事件循环正式结束!

第二轮事件循环:

代码语言:javascript
复制
a)、第二轮事件循环从宏任务setTimeout1开始。遇到console.log,立即执行。输出: 2
b)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务
队列中。记为process2
c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到
微任务队列中。记为then2。输出: 5

此时第二轮事件循环宏任务结束,下表是第二轮事件循环宏任务结束时各任务队列的情况

可以看到第二轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行

代码语言:javascript
复制
d)、执行process2。输出:3
e)、执行then2。输出:5

第二轮事件循环正式结束!

第三轮事件循环:

代码语言:javascript
复制
a)、第三轮事件循环从宏任务setTimeout2开始。遇到console.log,立即执行。输出: 9
d)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务
队列中。记为process3
c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到
微任务队列中。记为then3。输出: 11

此时第三轮事件循环宏任务结束,下表是第三轮事件循环宏任务结束时各任务队列的情况

可以看到第二轮事件循环宏任务结束后微任务队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行

代码语言:javascript
复制
d)、执行process3。输出:10
e)、执行then3。输出:12

第二轮事件循环正式结束! 执行栈清空,任务队列清空,事件循环正式结束!

参考: https://segmentfault.com/a/1190000017970432 http://www.ruanyifeng.com/blog/2014/10/event-loop.html

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-03-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.单线程
  • 2.同步任务和异步任务
    • 2.1同步(sync):
      • 2.2异步(async):
      • 3.事件循环
      • 4.执行栈和任务队列
      • 5.事件循环的具体实现过程?
      • 6.事件循环的例子
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档