详解JavaScript事件循环机制


Posted in Javascript onSeptember 07, 2018

众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心。可看HTML规范中的这段话:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,用户引擎必须使用 event loops。Event Loop 包含两类:一类是基于 Browsing Context ,一种是基于 Worker ,二者是独立运行的。 下面本文用一个例子,着重讲解下基于 Browsing Context 的事件循环机制。

来看下面这段 JavaScript 代码:

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

先猜测一下这段代码的输出顺序是什么,再去浏览器控制台输入一下,看看实际输出的顺序和你猜测出的顺序是否一致,如果一致,那就说明,你对 JavaScript 的事件循环机制还是有一定了解的,继续往下看可以巩固下你的知识;而如果实际输出的顺序和你的猜测不一致,那么本文下面的部分会为你答疑解惑。

任务队列

所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。具体的可以用下面的图来大致说明一下:

详解JavaScript事件循环机制

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:

  • 在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
  • 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
  • 更新 render

主线程重复执行上述步骤

可以用一张图来说明下流程:

详解JavaScript事件循环机制

这里相信有人会想问,什么是 microtasks ?规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task ,有些文章并没有对其做区分,后面文章中所提及的task皆看做宏任务( macro task)。

(macro)task 主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

setTimeout/Promise 等API便是任务源,而进入任务队列的是由他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中 setTimeout 与 setInterval 是同源的。

分析示例代码

千言万语,不如就着例子讲来的清楚。下面我们可以按照规范,一步步执行解析下上面的例子,先贴一下例子代码(免得你往上翻)。

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 作为第一个宏任务进入主线程,遇到 console.log,输出 script start

  • 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
  • 遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2
  • 遇到 console.log,输出 script end

至此,Event Queue 中存在三个任务,如下表:

宏任务 微任务
setTimeout then1
- then2
  • 执行微任务,首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有微任务
  • 执行 setTimeout 任务,输出 setTimeout 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout

so,你猜对了吗?

看看你掌握了没

再来一个题目,来做个练习:

console.log('script start');

setTimeout(function() {
 console.log('timeout1');
}, 10);

new Promise(resolve => {
 console.log('promise1');
 resolve();
 setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
 console.log('then1')
})

console.log('script end');

这个题目就稍微有点复杂了,我们再分析下:

首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到 promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1; 接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2 至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

用流程图看更清晰:

详解JavaScript事件循环机制

总结

有个小 tip:从规范来看,microtask 优先于 task 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。

最后的最后,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。。

Javascript 相关文章推荐
IE中jscript/javascript的条件编译
Sep 07 Javascript
几个常用的JavaScript字符串处理函数 - split()、join()、substring()和indexOf()
Jun 02 Javascript
浏览器打开层自动缓慢展开收缩实例代码
Jul 04 Javascript
鼠标拖拽移动子窗体的JS实现
Feb 25 Javascript
js图片轮播效果实现代码
Apr 18 Javascript
js基础之DOM中元素对象的属性方法详解
Oct 28 Javascript
jQuery焦点图左右转换效果
Dec 12 Javascript
vue 实现通过手机发送短信验证码注册功能
Apr 19 Javascript
js控制随机数生成概率代码实例
Mar 21 Javascript
微信小程序开发之点击按钮退出小程序的实现方法
Apr 26 Javascript
Node.js实现简单管理系统
Sep 23 Javascript
javascript 关于赋值、浅拷贝、深拷贝的个人理解
Nov 01 Javascript
解决vue 引入子组件报错的问题
Sep 06 #Javascript
解决vue v-for 遍历循环时key值报错的问题
Sep 06 #Javascript
vue 解决循环引用组件报错的问题
Sep 06 #Javascript
vue slots 组件的组合/分发实例
Sep 06 #Javascript
React注册倒计时功能的实现
Sep 06 #Javascript
node.js 模块和其下载资源的镜像设置的方法
Sep 06 #Javascript
Vue文件配置全局变量的实例
Sep 06 #Javascript
You might like
dedecms系统常用术语汇总
2007/04/03 PHP
PHP中替换换行符的几种方法小结
2012/10/15 PHP
php 根据自增id创建唯一编号类
2017/04/06 PHP
PHP设计模式(九)外观模式Facade实例详解【结构型】
2020/05/02 PHP
jQuery不间断滚动效果(模拟百度新闻支持文字/图片/垂直滚动)
2013/02/05 Javascript
jquery 单引号和双引号的区别及使用注意
2013/07/31 Javascript
原生JavaScript实现连连看游戏(附源码)
2013/11/05 Javascript
jQuery实现当按下回车键时绑定点击事件
2014/01/28 Javascript
js实现文本框输入文字个数限制代码
2015/12/25 Javascript
全屏js头像上传插件源码高清版
2016/03/29 Javascript
使用Node.js给图片加水印的方法
2016/11/15 Javascript
Bootstrap进度条学习使用
2017/02/09 Javascript
Bootstrap输入框组件使用详解
2017/06/09 Javascript
Bootstrap 模态对话框只加载一次 remote 数据的完美解决办法
2017/07/09 Javascript
Vue 过滤器filters及基本用法
2017/12/26 Javascript
如何使用 vue + d3 画一棵树
2018/12/03 Javascript
axios携带cookie配置详解(axios+koa)
2018/12/28 Javascript
浅谈vue限制文本框输入数字的正确姿势
2019/09/02 Javascript
[05:14]辉夜杯主赛事第二日 RECAP精彩回顾
2015/12/27 DOTA
Django 大文件下载实现过程解析
2019/08/01 Python
python使用多线程编写tcp客户端程序
2019/09/02 Python
python实现的读取网页并分词功能示例
2019/10/29 Python
关于python中plt.hist参数的使用详解
2019/11/28 Python
Python编程快速上手——强口令检测算法案例分析
2020/02/29 Python
python 模拟登录B站的示例代码
2020/12/15 Python
德国消费电子产品购物网站:Guter Kauf
2020/09/15 全球购物
Marlies Dekkers内衣荷兰官方网店:荷兰奢侈内衣品牌
2020/03/27 全球购物
创业计划书的主要内容有哪些
2014/01/29 职场文书
小学毕业感言300字
2014/02/19 职场文书
毕业典礼邀请函
2015/01/31 职场文书
个人欠条范本
2015/07/03 职场文书
初中生活随笔
2015/08/15 职场文书
分享15个Webpack实用的插件!!!
2021/03/31 Javascript
JVM上高性能数据格式库包Apache Arrow入门和架构详解(Gkatziouras)
2021/05/26 Servers
使用python绘制分组对比柱状图
2022/04/21 Python
Android开发手册TextInputLayout样式使用示例
2022/06/10 Java/Android