摘要:本文通过结合官方文档MDN和其他博客深入解析浏览器的事件循环机制,而NodeJS有另一套事件循环机制,不在本文讨论范围中。process.nextTick和setImmediate是NodeJS的API,所以本文也不予讨论。

首先,先了解几个概念。

Javascript到底是单线程还是多线程语言?


Javascript是一门单线程语言。相信应该有不少朋友对于Javascript是单线程语言还有些疑问(题外话:之前在某次面试中遇到一个面试官,一来就是“我们知道JS是一门多线程语言。。。”巴拉巴拉,当时就把我给愣住了。),不是有Web
Worker可以创建多个线程吗?答案就是,Javascript是单线程的,但是他的运行环境不是单线程。要如何理解这句话,首先得从Javascript运行环境比如浏览器的多线程说起。

浏览器通常包含以下线程:

  1. GUI渲染线程

    • 主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等。
    • 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
    • 该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起。
  2. JS引擎线程

    • 该线程负责处理Javascript脚本,执行代码。
    • 负责执行待执行的事件,比如定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待JS引擎线程执行。
    • 该线程与GUI线程互斥,当JS线程执行Javascript脚本事件过长,将导致页面渲染的阻塞。
  3. 定时器触发线程

    • 负责执行异步定时器一类函数的线程,如:setTimeout,setInterval。
    • 主线程依次执行代码时,遇到定时器会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕的事件回调加入到任务队列,等待JS引擎线程执行。
  4. 事件触发线程

    • 主要负责将等待执行的事件回调交给JS引擎线程执行。
  5. 异步http请求线程

    • 负责执行异步请求一类函数的线程,如:Promise,axios,ajax等。
    • 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列,等待JS引擎线程执行。

Web
Worker是浏览器为Javascript提供的一个可以在浏览器后台开启一个新的线程的API(类似上面说到浏览器的多个线程),使Javascript可以在浏览器环境中多线程运行,但这个多线程是指浏览器本身,是它在负责调度管理Javascript代码,让他们在恰当时机执行。所以Javascript本身是不支持多线程的。

异步


Javascript的异步过程通常是这样的:

  1. 主线程发起一个异步请求,异步任务接受请求并告知主线程已收到(异步函数返回);
  2. 主线程继续执行后续代码,同时异步操作开始执行;
  3. 异步操作执行完成后通知主线程;
  4. 主线程收到通知后,执行异步回调函数。

这个过程有个问题,异步任务各任务的执行时间过程长短不同,执行完成的时间点也不同,主线程如何调控异步任务呢?这就引入了消息队列。

栈、堆、消息队列


:函数调用形成的一个由若干帧组成的栈。

:对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域。

消息队列:一个Javascript运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用来处理这个消息的回调函数。在事件循环期间,运行时会从最先进入队列的消息开始处理,被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。然后事件循环在处理队列中的下一个消息。

事件循环Event loop


了解了上述要点,现在回到主题事件循环。那么Event loop到底是什么呢?

Event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event loop。
现在明白为什么要把NodeJS排除在外了吧?同样网上很多Event loop的相关博文一来就是Javascript的Event loop,实际上说的都是浏览器的Event loop。
浏览器的Event loop是在Html5规范中定义的,大致总结如下:

一个事件循环里有很多个任务队列(task queues)来自不同任务源,每一个任务队列里的任务(task)都是严格按照先进先出的顺序执行的,但是不同任务队列的任务执行顺序是不确定的,浏览器会自己调度不同任务队列。也有地方把task称之为macrotask(宏任务)。

规范中还提到了microtask(微任务)的概念,以下是规范阐述的进程模型:

  1. 选择当前要执行的任务队列,选择一个最先进入任务队列的任务,如果没有任务可以选择,则会跳转至microtask的执行步骤;
  2. 将事件循环的当前运行任务设置为已选择的任务;
  3. 运行任务;
  4. 将事件循环的当前任务设置为null,将运行完的任务从任务队列中移除;
  5. microtask步骤:进入microtask检查点;
  6. 更新界面渲染;
  7. 返回第一步。

执行进入microtask检查点时,用户代理会执行以下步骤:

  1. 设置进入microtask检查点的标志为true;
  2. 当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask,设置事件循环当前运行任务为此microtask;
  3. 运行microtask;
  4. 设置事件循环当前运行任务为null,将运行结束的microtask从microtask队列中移除;
  5. 对于相应事件循环的每个环境设置对象,通知它们哪些promise为rejected;
  6. 清理indexedDB的事务;
  7. 设置进入microtask检查点的标志为false。

由上可总结为:在事件循环中,用户代理会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志时函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行。

task/macrotask(宏任务)

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI rendering

microtask(微任务)

  • Promise.then catch finally
  • MutationObserver

来看一个例子:

console.log('script start');

setTimeout(function() {
console.log('setTimeout');
}, 0); Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
}); console.log('script end');

运行结果是:

script start
script end
promise1
promise2
setTimeout

那么问题来了,不是说每个事件循环开始会从task队列取最先进入的task执行,然后再执行所有microtask吗?为什么setTimeout是task却在Promise.then这个task的前面呢?反正我一开始是有这个疑惑的,很多文章都没有说清楚这个具体执行的顺序,大部分都是在描述规范的时候说的是“每个事件循环开始会从task队列中取一个task执行,然后再执行所有microtask”,但是也有部分文章说的是“每个事件循环开始都是先执行所有microtask”。经过本人多方查证,规范里的描述如上确实就是每个事件循环都是先执行task,那为什么上面例子里面体现出来的是先执行所有microtask呢?

script(整体代码)属于task。

来看一下上面例子的详细执行过程:

  1. 事件循环开始,task队列中只有一个script,选择script作为事件循环的已选择任务;
  2. script按顺序执行,同步代码直接输出(script start、script end);
  3. 遇到setTimeout,0ms后将回调函数放入task队列;
  4. 遇到Promise,将第一个then的回调函数放入microtask队列;
  5. 当所有script代码执行完成后,此时函数执行栈为空,开始检查microtask队列,队列只有第一个.then的回调函数,执行输出“promise1”,由于第一个.then返回的依然是promise,所以第二个.then的回调会放入microtask队列继续执行,输出“promise2”;
  6. 此时microtask队列空了,进入下一个事件循环,检查task队列取出setTimeout回调函数,执行输出“setTimeout”,代码执行完成。

这样是不是清楚了?所以实际上一开始执行script代码的时候就已经开始事件循环了,这就解释了为什么好像每次都是先执行所有的microtask。同时,这个例子中还引申出一个要点:在执行microtask任务的时候,如果又产生了新的microtask,那么会继续添加到队列的末尾,且也会在这个事件循环周期执行,直到microtask队列为空为止。

事件循环Event loop到底是什么的更多相关文章

  1. 简单了解一下事件循环(Event Loop)

    关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...

  2. JS事件循环(Event Loop)机制

    前言 众所周知,为了与浏览器进行交互,Javascript是一门非阻塞单线程脚本语言. 为何单线程? 因为如果在DOM操作中,有两个线程一个添加节点,一个删除节点,浏览器并不知道以哪个为准,所以只能选 ...

  3. 事件循环 event loop 究竟是什么

    事件循环 event loop 究竟是什么 一些概念 浏览器运行时是多进程,从任务管理器或者活动监视器上可以验证. 打开新标签页和增加一个插件都会增加一个进程,如下图:  浏览器渲染进程是多线程,包 ...

  4. 浏览器与Node的事件循环(Event Loop)有何区别?

    前言 本文我们将会介绍 JS 实现异步的原理,并且了解了在浏览器和 Node 中 Event Loop 其实是不相同的. 一.线程与进程 1. 概念 我们经常说 JS 是单线程执行的,指的是一个进程里 ...

  5. JavaScript事件循环(Event Loop)机制

    JavaScript 是单线程单并发语言 什么是单线程 主程序只有一个线程,即同一时间片断内其只能执行单个任务. 为什么选择单线程? JavaScript的主要用途是与用户互动,以及操作DOM.这决定 ...

  6. JavaScipt 中的事件循环(event loop),以及微任务 和宏任务的概念

    说事件循环(event loop)之前先要搞清楚几个问题. 1. js为什么是单线程的? 试想一下,如果js不是单线程的,同时有两个方法作用dom,一个删除,一个修改,那么这时候浏览器该听谁的?   ...

  7. JavaScript 事件循环 — event loop

    引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...

  8. 一文梳理JavaScript 事件循环(Event Loop)

    事件循环(Event Loop),是每个JS开发者都会接触到的概念,但是刚接触时可能会存在各种疑惑. 众所周知,JS是单线程的,即同一时间只能运行一个任务.一般情况下这不会引发问题,但是如果我们有一个 ...

  9. 事件循环Event Loop

    在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息.被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数.正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧. ...

随机推荐

  1. openstack高可用集群21-生产环境高可用openstack集群部署记录

    第一篇 集群概述 keepalived + haproxy +Rabbitmq集群+MariaDB Galera高可用集群   部署openstack时使用单个控制节点是非常危险的,这样就意味着单个节 ...

  2. NGINX镜像的制作

    NGINX镜像的制作 # mkdir -pv /opt/nginx # cd /opt/nginx/ # cat index.html www.dexter.com   编写Dockerfile # ...

  3. zabbix学习(一)——LNMP环境搭建及zabbix安装

    第一部分:LNMP环境搭建 一.环境说明: OS:   centos7.6_x64nginx:nginx-1.16.0php:   php-7.1.11mysql:mysql-5.6.44 zabbi ...

  4. codeforces 1443D,解法简单,思维缜密的动态规划问题

    大家好,欢迎来到codeforces专题. 今天选择的问题是1443场次的D题,这题是全场倒数第三题,截止到现在一共通过了2800余人.这题的思路不算难,但是思考过程非常有趣,这也是这一期选择它的原因 ...

  5. Windows无法访问共享文件夹

    问题描述 今天打开vss连接代码,提示如下信息 解决办法 可行:重置登录用户信息 原博文 https://zhidao.baidu.com/question/1174230805440255699.h ...

  6. python序列(六)列表排序

    1.sort方法排序原址排序 list.sort(key=None,reverse=False(or True)) 当reverse=False时:为正向排序: 当reverse=True时:为反向排 ...

  7. git使用上

    因为最近工作上多处都用到了基于 Git 的开发,需要深入理解 Git 的工作原理,以往的 Git 基本知识已经满足不了需求了,因此写下这篇 Git 进阶的文章,主要是介绍了一些大家平时会碰到但是很少去 ...

  8. freemarker读取session里面的值

    项目背景:springMVC+freemarker模板开发web 时代和信后台管理界面 代码示例: 后台服务: HttpSession session = request.getSession(); ...

  9. Hive日期函数总结(转学习使用)

    一.时间戳函数 1.获取当前时区的UNIX时间戳:select unix_timestamp(); 2.将指定时间转为UNIX时间戳: select unix_timestamp('2012-03-0 ...

  10. 阿里云centos7安装mysql8数据库

    一.安装mysql 1. mysql官网查找仓库源镜像,选择downloads https://www.mysql.com/downloads/ 2. 找到社区版 3. 选择yum仓库 4. 选择对应 ...