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


    浏览器的事件循环,前端再熟悉不过了,每天都会接触的东西。但我以前一直都是死记硬背:事件任务队列分为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>
    

    摄图网https://www.wode007.com/sites/73204.html VJ师网https://www.wode007.com/sites/73287.html

    经过再仔细看别人对任务队列的介绍,才知道,同步执行的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
    */
    

      

  • 相关阅读:
    uva 10561 sg定理
    二进制下 求分数化小数的循环节问题
    zoj 2562 反素数
    uva 11916 解模方程a^x=b (mod n)
    Android 组件化方案探索与思考
    2018谷歌I/O开发者大会8大看点汇总 新品有哪些
    Glide高级详解—缓存与解码复用
    Android性能优化:手把手带你全面实现内存优化
    Android几种强大的下拉刷新库
    Android app 在线更新那点事儿(适配Android6.0、7.0、8.0)
  • 原文地址:https://www.cnblogs.com/ypppt/p/13337876.html
Copyright © 2020-2023  润新知