深入浏览器事件循环的本质

更新日期: 2018-11-09 阅读: 4.1k 标签: 浏览器

浏览器的事件循环,前端再熟悉不过了,每天都会接触的东西。但我以前一直都是死记硬背:事件任务队列分为macrotask和microtask,浏览器先从macrotask取出一个任务执行,再执行microtask内的所有任务,接着又去macrotask取出一个任务执行...,这样一直循环下去。但是对于下面的代码,我一直懵逼,setTimeout属于macrotask,按照上面的规则,setTimeout应该先被取出来执行啊,但是我却被执行结果打脸了。

<script>
    setTimeout(() => {
        console.log(1)
    }, 0)
    new Promise((resolve) => {
        console.log(2)
        resolve()
    }).then(() => {
        console.log(3)
    })
    // 我曾经的预期是:2 1 3
    // 实际输出:2 3 1
</script>

经过再仔细看别人对任务队列的介绍,才知道,同步执行的js代码其实就算一个macrotask(准确说是每一个script标签内的代码都是一个macrotask),所以上面的规则中说的 先取出一个macrotask执行 是没有问题的。
网上很多文章都是像上面这样解释的,我也一直认为这是html对事件循环的规范,我们记着就是。直到最近看了李银城大佬的文章(见文末的参考链接),我才恍然大悟,之前看的文章都没有明确地从浏览器的多线程模型这个角度分析,所以让我们觉得浏览器的事件循环是基于上述的约定,但其实这是浏览器的多线程模型导致的结果。

macrotask的本质

macrotask本质上是浏览器多个线程之间通信的一个消息队列
在chrome里,每个页面都对应一个进程,该进程又有多个线程,比如js线程、渲染线程、io线程、网络线程、定时器线程等,这些线程之间的通信,是通过向对方的任务队列中添加一个任务(PostTask)来实现的。

浏览器的各种线程都是常驻线程,它们运行在一个for死循环里面,每个线程都有属于自己的若干任务队列,线程自己或者其它线程都可能通过PostTask向这些任务队列添加任务,这些线程会不断地从自己的任务队列中取出任务执行,或者是处于睡眠状态直到设定的时间或者是有人PostTask的时候把它们唤醒。

可以简单地理解为,浏览器的各个线程都在不停地从自己的任务队列中取出任务,执行,再取出任务,再执行,这样无限循环下去。

以下面的代码为例:

<script>
    console.log(1)
    setTimeout(() => {
        console.log(2)
    }, 1000)
    console.log(3)
</script>
  1. 首先,script标签中的代码作为一个任务放入js线程的任务队列,js线程被唤醒,然后取出该任务执行
  2. 首先执行console.log(1),然后执行setTimeout,向定时器线程添加一个任务,接着执行console.log(3),这时js线程的任务队列为空,js线程进入休眠
  3. 大约1000ms后,定时器线程向js线程的任务队列添加定时任务(定时器的回调),js线程又被唤醒,执行定时回调函数,最后执行console.log(2)。

可以看到,所谓的macrotask并不是浏览器定义了哪些任务是macrotask,浏览器各个线程只是忠实地循环自己的任务队列,不停地执行其中的任务而已。

microtask

比起macrotask是浏览器的多线程模型造成的“假象”,microtask是确实存在的一个队列,microtask是属于当前线程的,而不是其他线程PostTask过来的任务,只是延迟执行了而已(准确地说是放到了当前执行的同步代码之后执行),比如Promise.then、MutationObserver都属于这种情况。

以下面的代码为例:

<script>
    new Promise((resolve) => {
       resolve()
       console.log(1)
       setTimeout(() => {
         console.log(2)
       },0)
    }).then(() => {
        console.log(3)
    })
    // 输出:1 3 2
</script>
  1. 首先,script标签中的代码作为一个任务放入js线程的任务队列,js线程被唤醒,然后取出该任务执行
  2. 然后执行new Promise以及Promise中的resolve,resolve后,promise的then的回调函数会作为需要延迟执行的任务,放到当前执行的所有同步代码之后
  3. 接着执行setTimeout,向定时器线程添加一个任务
  4. 此时同步代码执行完毕,接着执行被延迟执行的任务,也就是promise的then的回调函数,即执行console.log(3)
  5. 最后,js线程的任务队列为空,js线程进入休眠,大约1000ms后,定时器线程向js线程的任务队列添加定时任务(定时器的回调),js线程又被唤醒,执行定时回调函数,即console.log(2)。

总结

通过上面的分析,可以看到,文章开头提到的规则:浏览器先从macrotask取出一个任务执行,再执行microtask内的所有任务,接着又去macrotask取出一个任务执行...,并没有说错,但这只是浏览器执行机制造成的现象,而不是说浏览器按照这样的规则去执行的代码。
这篇文章中的所有干货都来自李银成大佬的文章,我只是按照自己的理解,做了简化描述,方便大家理解,也加深自己的印象。
最后,看了这篇文章,大家能够基于浏览器的运行机制,分析出下面代码的执行结果了吗(ps:不要用死记硬背的规则去分析哟)

console.log('start')

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')
    })
// 执行结果
/*  start
    promise 1
    promise 2
    setInterval
    setTimeout 1
    promise 3
    promise 4
    setInterval
    setTimeout 2
    promise 5
    promise 6
*/


参考:从Chrome源码看事件循环
来源:https://segmentfault.com/a/1190000016955318


本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/1304

相关推荐

浏览器禁用了javascript,各种浏览器如何开启javascript的方法总汇

您的浏览器禁用了JS脚本运行,请启用该功能。怎么解除浏览器禁用js?这篇文章将总结整理各个浏览器如何开启、禁用javascript的方法总汇。

监听浏览器刷新及关闭

为保证‘高度安全性’,用户每次退出页面或浏览器都要清除登陆信息,每次进入系统都要重新登陆(每次登陆还要手机验证码等乱七八糟的验证信息,,,求用户的心里阴影面积),但是刷新页面不可以清除登陆信息。

Js实现阻止浏览器返回的功能

无论pc端还是移动端,浏览器都会带有后退按钮或后退键.主要方便我们能返回以前访问过的页面,但有时候我们不得不关闭这个功能.尤其是对于一些推广落地页,用户进入后不希望它返回

window.open被拦截的解决方法总汇

介绍window.open方法被浏览器拦截的处理方式。在 Chrome 的安全机制里,非用户直接触发的 window.open 方法,是会被拦截的,这是由于浏览器为了维护用户安全和体验,下面采用几种变通方法解决:表单提交的方式、onclick事件、延迟打开等

Chrome浏览器crx格式插件安装教程

谷歌浏览器在旧版本(大概是v67版本)之前安装crx插件都非常简单,直接将crx拖放到浏览器内就可以安装了。但是之后的新版本(目前已经升级到v80版本)就只允许用户通过谷歌应用商店安装插件

如何将网站设置为浏览器首页

提示:按 Ctrl + D 即可添加网址到浏览器收藏夹中,方便下次访问fly63导航。下面是如何设置首页的方法。Google Chrome浏览器设为首页的方法;Firefox火狐浏览器设为首页的方法

完美解决安卓端百度浏览器屏蔽fixed悬浮元素的问题

h5活动页面底部有个悬浮图片按钮,使用fixed悬浮定位在底部,但是在安卓端的百度浏览器下打开,却发现该图片一闪而过,在百度浏览器中消失不见。

Fiddler无法正常抓取谷歌等浏览器的请求_解决方案

fiddler会自动给浏览器设置一个代理127.0.0.1端口8888,并且记忆浏览器的代理设置,所有的请求先走fiddler代理,再走浏览器代理。解决方案:关闭SwitchyOmega代理,或者使用其代理中的系统代理选项。即可解决问题。

js判断浏览器内核是否是safari浏览器

PC端只有Chrome有Safari字段吗?为什么不需要判断其他浏览器?其实360,QQ等浏览器的userAgent字段也会带有Safari字段,但是由于他们基于Chrome二次开发的,所有也会携带有Chrome字段。

Vue单页面应用阻止浏览器记住密码

现象1:路由切换时再次提示是否记住密码,解决办法:这其实是个代码bug,在登录页面,用form把输入框都包起来就行了。现象2:autocomplete=off无效

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!