详解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 相关文章推荐
超级退弹代码
Jul 07 Javascript
JS 文件传参及处理技巧分析
May 13 Javascript
js控制不同的时间段显示不同的css样式的实例代码
Nov 04 Javascript
JavaScript判断访问的来源是手机还是电脑,用的哪种浏览器
Dec 12 Javascript
教你用javascript实现随机标签云效果_附代码
Mar 16 Javascript
基于MVC5和Bootstrap的jQuery TreeView树形控件(一)之数据支持json字符串、list集合
Aug 11 Javascript
jQuery将表单序列化成一个Object对象的实例
Nov 29 Javascript
js+html5实现侧滑页面效果
Jul 15 Javascript
Bootstrap与Angularjs的模态框实例代码
Aug 03 Javascript
ReactNative Image组件使用详解
Aug 07 Javascript
vue超时计算的组件实例代码
Jul 09 Javascript
一文彻底理解js原生语法prototype,__proto__和constructor
Oct 24 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
PHP使用PDO创建MySQL数据库、表及插入多条数据操作示例
2019/05/30 PHP
自己写的兼容ie和ff的在线文本编辑器类似ewebeditor
2012/12/12 Javascript
JavaScript 数组详解
2013/10/10 Javascript
fixedBox固定div漂浮代码支持ie6以上大部分主流浏览器
2014/06/26 Javascript
jQuery ajax全局函数处理session过期后的ajax跳转问题
2016/06/03 Javascript
JS二叉树的简单实现方法示例
2017/04/05 Javascript
jQuery实现打开网页自动弹出遮罩层或点击弹出遮罩层功能示例
2017/10/19 jQuery
微信小程序 Animation实现图片旋转动画示例
2018/08/22 Javascript
JS数组的常用10种方法详解
2020/05/08 Javascript
[03:48]2014DOTA2 TI专访71DK夺冠不靠小组赛高排名
2014/07/11 DOTA
[01:05:41]EG vs Optic Supermajor 败者组 BO3 第二场 6.6
2018/06/07 DOTA
wxpython学习笔记(推荐查看)
2014/06/09 Python
Python基于PycURL实现POST的方法
2015/07/25 Python
Python的“二维”字典 (two-dimension dictionary)定义与实现方法
2016/04/27 Python
Python如何实现MySQL实例初始化详解
2017/11/06 Python
selenium+python自动化测试之鼠标和键盘事件
2019/01/23 Python
numpy 声明空数组详解
2019/12/05 Python
使用Python来做一个屏幕录制工具的操作代码
2020/01/18 Python
matlab、python中矩阵的互相导入导出方式
2020/06/01 Python
爬虫代理的cookie如何生成运行
2020/09/22 Python
HTML5中的音频和视频媒体播放元素小结
2016/01/29 HTML / CSS
德国购买健身器材:AsVIVA
2017/08/09 全球购物
Expedia英国:全球最大的在线旅游公司
2017/09/07 全球购物
美国知名的旅游网站:OneTravel
2018/10/09 全球购物
意大利在线购买隐形眼镜网站:VisionDirect.it
2019/03/18 全球购物
英国羊皮鞋类领先品牌:Just Sheepskin
2019/12/12 全球购物
.NET初级开发工程师面试题
2014/04/18 面试题
举例说明类变量和实例变量的区别
2016/06/30 面试题
最新创业融资计划书
2014/01/19 职场文书
退学证明范本3篇
2014/10/29 职场文书
陕西导游词
2015/02/04 职场文书
教师求职自荐信范文
2015/03/04 职场文书
一般纳税人申请报告
2015/05/18 职场文书
高中政治教学反思
2016/02/23 职场文书
Python+Appium新手教程
2021/04/17 Python
vue实现input输入模糊查询的三种方式
2022/08/14 Vue.js