浏览器中的事件循环机制

网上一搜事件循环, 很多文章标题的前面会加上 JavaScript, 但是我觉得事件循环机制跟 JavaScript 没什么关系, JavaScript 只是一门解释型语言, 方便开发和理解的, 由V8 JIT将 JavaScript 编译成机器语言来调用底层, 至于浏览器怎么执行 JavaScript 代码, JavaScript 管不着也不关心. 因此, “JavaScript事件循环机制”这种说法是不合理的. 事件循环机制是由运行时环境实现的, 具体来说有浏览器、Node等. 这篇文章就先来说说浏览器中实现的事件循环机制.

正文

首先,javascript 在浏览器端运行是单线程的,这是由浏览器决定的,这是为了避免多线程执行不同任务会发生冲突的情况。也就是说我们写的javascript 代码只在一个线程上运行,称之为主线程(HTML5提供了web worker API可以让浏览器开一个线程运行比较复杂耗时的 javascript任务,但是这个线程仍受主线程的控制)。单线程的话,如果我们做一些“sleep”的操作比如说:

var now = + new Date()
while (+new Date() <= now + 1000){
//这是一个耗时的操所
}

那么在这将近一秒内,线程就会被阻塞,无法继续执行下面的任务。

还有些操作比如说获取远程数据、I/O操作等,他们都很耗时,如果采用同步的方式,那么进程在执行这些操作时就会因为耗时而等待,就像上面那样,下面的任务也只能等待,这样效率并不高。

那浏览器是怎么做的呢?

我们找到WHATWG规范对Event loop的介绍:

为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用事件循环。

事件循环的主要机制就是任务队列机制:

  • 一个事件循环有一个或者多个任务队列(task queues)。任务队列是task的有序列表,task是调度Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation这些任务的算法;
  • 每个任务都来自一个特定的任务源(task source)(比如鼠标键盘事件)。来自同一个特定任务源且属于特定事件循环的任务必须被加入到同一个任务队列中,来自不同任务源的任务可以放在不同的任务队列中;
  • 浏览器调用这些队列中的任务时采取这样的做法: 相同队列中的任务按照先进先出的顺序, 不同的队列按照提前设置的队列优先级来调用. 例如,用户代理可以有一个用于鼠标和键盘事件的任务队列(用户交互任务源),另一个用于其他任务。然后,用户代理75%概率调用键盘和鼠标事件任务队列,25%调用其他队列, 这样的话就保持界面响应而且不会饿死其他任务队列. 但是相同队列中的任务要按照先进先出的顺序。也就是说单独的任务队列中的任务总是按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行

在调用任务的过程中, 会产生新的任务, 浏览器就会不断执行任务, 因此称为事件循环.

microtask queue 微任务队列

还有一些特殊任务, 它们不会被放在task queues中, 会放在一个叫做microtask(微任务) queue中, 继续看标准:

Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.

任务队列可以有多个, 但是微任务队列只有一个.

那么哪些任务是放在task queue, 哪些放在microtask queue呢? 通常对浏览器和Node.js来说:

  • macrotask(宏任务): script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering等
  • microtask(微任务): process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver

请尤其注意macrotask中执行整体代码也是一个宏任务

事件循环处理过程

总体来说, 浏览器端事件循环的一个回合(go-around或者叫cycle)就是:

  • 从macrotask队列中(task queue)取一个宏任务执行, 执行完后, 取出所有的microtask执行.
  • 重复回合

无论在执行macrotask还是microtask, 都有可能产生新的macrotask或者microtask, 就这样继续执行.

用任务队列机制解释异步操作顺序

这里有一些常见异步操作:

const interval = setInterval(() => {console.log('setInterval')
}, 0)setTimeout(() => {  console.log('setTimeout 1')Promise.resolve().then(() => {console.log('promise 3')}).then(() => {console.log('promise 4')}).then(() => {setTimeout(() => {console.log('setTimeout 2')Promise.resolve().then(() => {console.log('promise 5')}).then(() => {console.log('promise 6')}).then(() => {clearInterval(interval)})}, 0)})
}, 0)Promise.resolve().then(() => {console.log('promise 1')
}).then(() => {console.log('promise 2')
})

结果(Chrome 63.0.3239.84; Mac OS):

promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval // 大部分情况下2次, 少数情况下一次
setTimeout 2
promise 5
promise 6

这个顺序是如何得来的?

我们先讲promise 4后面只出现一次setInterval的情况, 画个图简单表示一下这个过程:

注意

本图为了方便把各时间段(Cycle)队列的任务都画在队列中去了, 实际上执行一个task 和 microtask 后就会把这个任务从相应队列中删除

首先, 主任务就是执行脚本, 也就是执行上述代码, 这也是一个task. 在执行代码过程中, 遇到setTimeout、setInterval 就会将回调函数添加到task queue中, 遇到 promise 就会将then回调添加到 microtask 中去.

Task执行完, 接着取所有 microtask 执行, 所有microtask 执行完了, microtask queue也就空了, 接着再取task执行, 如果microtask queue为空, 没有任务, 则继续取下一个task执行, 就这样循环执行. 图中箭头就表示执行的顺序.

那么为什么promise 4后面大部分情况下出现2次setInterval, 少数情况出现1次呢?

我猜测这是因为setInterval是有最短间隔时间的(chrome下4ms左右), 这个时间不同机子、不同浏览器都有可能不一样. 代码中的参数是0, 意味着尽可能短的时间内就会产生一个task加入到 task queue中. 浏览器在执行setInterval后到执行下一个task前, 时间间隔就可能超过这个最短时间, 因此会产生一个setInterval task.

我是这样论证的:

我把含有promise5、promise6回调函数的setTimeout的时间设置大一点, 让它推迟插入task queue中:

...
setTimeout(() => {console.log('setTimeout 2')Promise.resolve().then(() => {console.log('promise 5')}).then(() => {console.log('promise 6')}).then(() => {clearInterval(interval)})
}, 10)   //这里加上10ms
...

结果是promise 4后面的setInterval出现了5次, 因此我觉得promise 4后面大部分情况下出现2次setInterval、少数情况出现一次的原因就是浏览器在执行setInterval回调函数后、执行setTimeout回调函数前, 时间间隔大部分情况超过了这个最短时间.

另外, 我试着再依次加上1ms, 直到14ms——也就是加上4ms时, promise 4后面的setInterval变成了6次, 可以认为setInterval最短间隔时间在Chrome下约为4ms(不考虑机子性能、设置).

Node中的奇怪结果

首先说明一下, 在Node中也体现了任务队列的机制, 但是这不是Node实现的, 这是V8实现的, 由Node调用了V8任务队列机制的API. 至于为什么是V8实现的, 我们翻翻ECMA 262 标准对 Job 和 Job queue 的介绍就可以得知

但是让人摸不着头脑的是, 这段代码在node v8.5.0下有时会出现这样的结果:

promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
setInterval   // 为什么会出现setInterval???
promise 5
promise 6

按理说应该是setTimeout 2 => promise 5 => promise 6, 因为输出setTimeout 2的回调函数是task, 执行完这个task后应该调用microtask 输出promise 5 => promise 6啊? 很奇怪! Node对V8确实有些改动, 不知道是不是这方面原因...

还请大神解惑!

你竟然读到这了

总结一下:

学习技术还是有捷径的, 那就是读标准 ;)

浏览器中的事件循环机制相关推荐

  1. js中如何得到循环中的点击的这个id_Js篇面试题9请说一下Js中的事件循环机制

    虽互不曾谋面,但希望能和您成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 标星公众号(ID:itclanCoder) 如果不知道如 ...

  2. Js篇-面试题9-请说一下Js中的事件循环机制

    虽互不曾谋面,但希望能和您成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 标星公众号(ID:itclanCoder) 如果不知道如 ...

  3. 详解浏览器和Node的事件循环机制及区别

    关于事件循环机制(详解) 前言 一.浏览器的事件循环机制 二.Node的事件循环机制 三.两者的区别 前言 JS是单线程的脚本语言,即在同一时间只能做一件事.为了协调时间.用户交互.脚本.UI渲染和网 ...

  4. boost log 能不能循环覆盖_前端基础进阶(十四):深入核心,详解事件循环机制...

    Event Loop JavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了.为了解决自己的这个困扰,在学习的过程中,我一直在试图 ...

  5. js的事件循环机制:同步与异步任务(setTimeout,setInterval)宏任务,微任务(Promise,process.nextTick)...

    javascript是单线程,一切javascript版的"多线程"都是用单线程模拟出来的,通过事件循环(event loop)实现的异步. javascript事件循环 事件循环 ...

  6. JS事件循环机制:同步与异步任务 之 宏任务 微任务

    JS事件循环机制:同步与异步任务 之 宏任务 微任务 阅读目录 javascript事件循环 setTimeout和setInterval中的执行时间 宏任务和微任务 javascript是单线程,一 ...

  7. 浏览器与node事件循环

    我们都知道在浏览器中由于dom操作,js是单线程的. 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得 ...

  8. js事件循环机制(await-async-事件循环)

    await和async 异步函数 async function async关键字用于声明一个异步函数: async是asynchronous单词的缩写,异步.非同步: sync是synchronous ...

  9. JavaScript重难点解析5(对象高级、浏览器内核与事件循环模型(js异步机制))

    JavaScript重难点解析5(对象高级.浏览器内核与事件循环模型(js异步机制) 对象高级 对象创建模式 Object构造函数模式 对象字面量模式 工厂模式 自定义构造函数模式 构造函数+原型的组 ...

最新文章

  1. centos6.7 64位环境下部署MySQL-5.7.13
  2. MS SQL 维护小记
  3. JAVA 运行与开发环境配置(二)- hello java
  4. Serverless 风起云涌,为什么阿里,微软,AWS 却开始折腾 OAM?
  5. 暗影精灵4适合计算机专业,暗影精灵4pro怎么样_RTX系显卡带来的不仅是光追-太平洋电脑网...
  6. Text Link Ads 注册[赚钱一]
  7. Scrum Master需要具备哪些能力和经验
  8. JDBC 连接数据库,包含连接池
  9. 【公告】个人站点及系列文章
  10. Linux 修改 Tomcat 编码
  11. 已安装的Flash Player不支持FlexBuilder调试
  12. 发达国家如何布局大数据战略
  13. linux用户目录互信,linux 互信不生效
  14. 战争游戏红龙mod简介
  15. 这个男人用8幅画刻画“父爱”,被暖化了
  16. Flow-edge Guided Video Completion
  17. “Android开发3年老板嫌我工资高,把我辞了
  18. android 自动设置时区,Android Things入门-设置本地时区
  19. AI中的变形,扭曲和变化,栅格化命令,裁剪标记,路径,路径查找器,转化为形状
  20. Android备份到电脑,用 TWRP「一键 Ghost」你的 Android 手机,还能备份到电脑

热门文章

  1. 视觉SLAM——ceres非线性最小二乘求解器
  2. C++之指针探究(十八):typedef结合结构体指针
  3. ios php tpbase64编码,iOS Base64编码
  4. android方向触摸事件,Android触摸事件传递机制,这一篇就够了
  5. python的魔法方法
  6. python图例重复显示_python – 具有两个标记的多行的自定义图例,用于相同的文本...
  7. nuxt页面跳转_还不会Nuxt.js的速进!
  8. pytorch WHAT IS TORCH.NN REALLY?
  9. sqlalchemy 聚合
  10. 图解TCPIP-以太网(物理层)