专栏首页前端之旅深入理解事件循环

深入理解事件循环

本篇博客讲的东西偏底层,较难理解。虽然有的地方不够精准和全面,但是我觉得对于理解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

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

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

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')
    })
})

分析:

第一轮事件循环:

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

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

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

g)、执行process1。输出:6
h)、执行then1。输出:8

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

第二轮事件循环:

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

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

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

d)、执行process2。输出:3
e)、执行then2。输出:5

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

第三轮事件循环:

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

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

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

d)、执行process3。输出:10
e)、执行then3。输出:12

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深入理解事件

    javascript 给 DOM 绑定事件处理函数总的来说有2种方式:在 html 文档中绑定、在 js 代码中绑定。下面的方式1、方式2属于在 html 中绑...

    Chor
  • 折腾博客系列之发布自己的主题:PureBlue

    博客内容固然是最重要的,但是抛开内容不讲,博客本身也应该带有自己的Tag,而不是光会用别人的轮子。

    Chor
  • 编译原理学习笔记-4:词法分析(二)等价转换与DFA的化简

    正规文法(四元式)定义了某种正规语言,正规式表示了某个正规集,它也定义了某种正规语言,因此可以说正规式和正规文法是等价的。即:

    Chor
  • windows下出现mysql启动出现 ‘发生系统错误’ 1067

    今天在windows下安装mysql,在启动时出现了发生‘系统错误 1067’的错误。

    Java架构师历程
  • iOS多线程:GCD使用介绍

    最近作者在进行多线程问题排查和整理时,发现了好多问题都是由于GCD的使用不规范造成的,因此在这里主要分享GCD的使用方法,希望大家能够在测试时更早发现问题。

    用户5521279
  • 微信后台团队最近开源力作:PhxQueue 分布式队列

    PhxQueue 是微信开源的一款基于 Paxos 协议实现的高可用、高吞吐和高可靠的分布式队列,保证At-Least-Once Delivery,在微信内部广...

    腾讯开源
  • AMQP概述

    AMQP通常会定义服务器端域模型,以用于规范服务器的行为。 AMQP服务端通常称为AMQP Broker AMQP的三层协议分别是:Model层,Session...

    院长技术
  • 网易蜂巢上搭建CI服务

    最近由于工作需要,在不同的服务器上安装了好几遍 Gitlab Runner,由于资料较为分散,时间久了,有些安装步骤必然会有所遗忘。本文演示如何...

    前端黑板报
  • [C++数值算法]

    本书选材内容丰富,除了通常数值方法课程的内容外,还包含当代科学计算大量用到的专题,如求特殊函数值、随机数、排序、最优化、快速傅里叶变换、谱分析、小波变换、统计描...

    用户3157710
  • C#判断画的图形是不是三角形

    拾点阳光

扫码关注云+社区

领取腾讯云代金券