zoukankan      html  css  js  c++  java
  • 从几道题目带你深入理解Event Loop_宏队列_微队列

    深入探究JavaScript的Event Loop

    Javascript是一门单线程语言

    但是在运行时难免会遇到需要较长执行时间的任务如: 向后端服务器发送请求。 其他的任务不可能都等它执行完才执行的(同步)否则效率太低了, 于是异步的概念就此产生: 当遇到需要较长时间的任务时将其放入"某个地方"后继续执行其他同步任务, 等所有同步任务执行完毕后再poll(轮询)刚刚这些需要较长时间的任务并得到其结果

    而处理异步任务的这一套流程就叫Event Loop即事件循环,是浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制, 于是更完善的说法是: Javascript是一门单线程非阻塞语言

    Event Loop的结构

    • 堆(heap): 用于存放JS对象的数据结构
    • 调用栈(stack): 同步任务会按顺序在调用栈中等待主线程依次执行
    • Web API: 是浏览器/Node 用于处理异步任务的地方
    • 回调队列(callbacks queue): 经过Web API处理好的异步任务会被一次放入回调队列中, 等一定条件成立后被逐个poll(轮询)放入stack中被主线程执行

    回调队列(callbacks queue)的分类

    回调队列(callbacks queue)进而可以细分为

    1. 宏任务(macroTasks)

      • script全部代码、
      • setTimeout、
      • setInterval、
      • setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、
      • I/O、UI Rendering
    2. 微任务(microTasks)

      • Process.nextTick(Node独有)
      • MutationObserver
      • Promise、
      • Object.observe(废弃)

    Event Loop的执行顺序

    1. 首先顺序执行初始化代码(run script), 同步代码放入调用栈中执行, 异步代码放入对应的队列中
    2. 所有同步代码执行完毕后,确认调用栈(stack)是否为空, 只有stack为为空才能开始按照队列的特性轮询执行 微任务队列中的代码
    3. 只有当所有微任务队列中的任务执行完后, 才能执行宏任务队列中的下一个任务

    用流程图表示:

    通过题目来深入

    题目1:

    setTimeout(() => {
        console.log(1)
    }, 0)
    Promise.resolve().then(
        () => {
            console.log(2)
        }
    )
    Promise.resolve().then(
        () => {
            console.log(4)
        }
    )
    console.log(3)
    
    1. 执行初始化代码

    2. 初始化代码执行完毕, script 任务结束, 调用栈为空 所以可以开始轮询执行微任务队列的代码

      1. 取出第一个微任务到调用栈中执行--打印2, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

      2. 取出第二个微任务到调用栈中执行--打印4, 执行完后调用栈为空, 微任务队列为空

    3. 第一个宏任务(run script)完成, 可以轮询宏任务队列的下一个任务

    4. 开始轮询执行宏任务队列中的下一个任务

    5. 最终整个执行顺序、结果如图所示:

    于是这道题最终的结果是:

    3 2 4 1
    

    题目2:

    setTimeout(()=>{
        console.log(1)
    }, 0)
    
    
    new Promise((resolve, reject) => {
        console.log(2)
        resolve()
    })
    .then(
        () => {
            console.log(3)
        }
    )
    .then(
        () => {
            console.log(4)
        }
    )
    console.log(5)
    
    1. 执行初始化代码

    2. 初始化代码执行完毕, script 任务结束, 调用栈为空所以可以开始轮询执行微任务队列的代码

      1. 取出第一个微任务到调用栈中执行--打印3, 执行完后调用栈为空, 此时第一个then()返回的Promise有了状态、结果, 于是将第二个then()放入微任务队列中, 检查微任务队列是否还有任务有则执行

      1. 调用栈、微任务队列为空, run script执行完毕
    3. 开始轮询执行宏任务队列中的下一个任务

    4. 最终整个执行顺序、结果如图所示:

    于是这道题最终的结果是:

    2 5 3 4 1
    

    题目3:

    const first = () => {
        return new Promise((resolve, reject) => {
            console.log(3)
            let p = new Promise((resolve, reject) => {
                console.log(7)
                setTimeout(() => {
                    console.log(5)
                }, 0)
                resolve(1)
            })
            resolve(2)
            p.then(
                arg => {
                    console.log(arg)
                }
            )
        })
    }
    
    first().then(
        arg => {
            console.log(arg)
        }
    )
    
    console.log(4)
    
    1. 执行初始化代码

    2. 初始化代码执行完毕, script 任务结束, 调用栈为空所以可以开始轮询执行微任务队列的代码

      1. 取出第一个微任务到调用栈中执行--打印1, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

      1. 调用栈、微任务队列为空, run script执行完毕
    3. 开始轮询执行宏任务队列中的下一个任务

    4. 最终整个执行顺序、结果如图所示:

    于是这道题最终的结果是:

    3 7 4 1 2 5
    

    题目4:

    setTimeout(()=>{
        console.log(0)
    }, 0)
    
    
    new Promise((resolve, reject) => {
        console.log(1)
        resolve()
    })
    .then(
        () => {
            console.log(2)
            new Promise((resolve, reject) => {
                console.log(3)
                resolve()
            })
            .then(
                () => console.log(4)
            )
            .then(
                () => console.log(5)
            )
        }
    )
    .then(
        () => console.log(6)
    )
    
    new Promise((resolve, reject) => {
        console.log(7)
        resolve()
    })
    .then(
        () => console.log(8)
    )
    
    1. 执行初始化代码

    2. 初始化代码执行完毕, script 任务结束=, 所以可以开始轮询执行微任务队列的代码

      1. 取出第一个任务到调用栈--执行onResolved中的所有代码

      2. 很重要的地方是此时第一个new Promise的第二个then此时会被放入微任务队列中

      3. 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

      1. 调用栈、微任务队列为空, run script执行完毕
    3. 开始轮询执行宏任务队列中的下一个任务

    4. 最终整个执行顺序、结果如图所示:

    于是这道题最终的结果是:

    1 7 2 3 8 4 6 5 0
    

    题目5:

    console.log('script start')
    
    async function async1() {
        await async2()
        console.log('async1 end')
    }
    async function async2() {
        console.log('async2 end')
    }
    async1()
    
    setTimeout(function () {
        console.log('setTimeout')
    }, 0)
    
    new Promise(resolve => {
        console.log('Promise')
        resolve()
    })
    .then(function () {
        console.log('promise1')
    })
    .then(function () {
        console.log('promise2')
    })
    
    console.log('script end')
    
    1. 执行初始化代码

    2. 初始化代码执行完毕, script 任务结束=, 所以可以开始轮询执行微任务队列的代码

      1. 取出第一个任务到调用栈--执行await后的所有代码, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

      1. 调用栈、微任务队列为空, 宏任务run script执行完毕
    3. 开始轮询执行宏任务队列中的下一个任务

    4. 最终整个执行顺序、结果如图所示:

    于是这道题最终的结果是:

    script start
    async2 end
    Promise
    script end
    async1 end
    promise1
    promise2
    setTimeout
    

    终极题1:

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <style>
            .outer {
                 200px;
                height: 200px;
                background-color: orange;
            }
    
            .inner {
                 100px;
                height: 100px;
                background-color: salmon;
            }
        </style>
    </head>
    
    <body>
        <div class="outer">
            <div class="inner"></div>
        </div>
    
        <script>
            var outer = document.querySelector('.outer')
            var inner = document.querySelector('.inner')
    
            new MutationObserver(function () {
                console.log('mutate')
            }).observe(outer, {
                attributes: true,
            })
    
            function onClick() {
                console.log('click')
    
                setTimeout(function () {
                    console.log('timeout')
                }, 0)
    
                Promise.resolve().then(function () {
                    console.log('promise')
                })
    
                outer.setAttribute('data-random', Math.random())
            }
    
            inner.addEventListener('click', onClick)
            outer.addEventListener('click', onClick)
        </script>
    </body>
    </html>
    
    1. 执行初始化代码

    2. 初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码

      1. 取出第一个任务到调用栈--打印promise, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

      1. 调用栈、微任务队列为空, 因为存在冒泡, 所以以上操作再进行一次
    3. 宏任务run script执行完毕, 调用栈、微任务队列为空可以轮询执行宏任务队列中的下一个任务

    4. 开始轮询执行宏任务队列中的下一个任务

    5. 微任务队列、调用栈为空, 继续轮询执行宏任务队列中的下一个任务

    于是这道题最终的结果是:

    click
    promise
    mutate
    click
    promise
    mutate
    timeout
    timeout
    

    不同浏览器下的不同结果(如果你的结果在这其中, 也是对的)

    这里令人迷惑的点是: outer的冒泡执行为什么比outer的setTimeout先

    那是因为:

    • 首先outer的setTimeout是一个宏任务, 它进入宏任务队列时是在了run script的后面
    • inner执行到mutate后run script并没有执行完, 而是还有一个outer.click的冒泡要执行
    • 只有执行完该冒泡后, run script才真正执行完(才可以执行下一个宏任务)

    终极题2:

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <style>
            .outer {
                 200px;
                height: 200px;
                background-color: orange;
            }
    
            .inner {
                 100px;
                height: 100px;
                background-color: salmon;
            }
        </style>
    </head>
    
    <body>
        <div class="outer">
            <div class="inner"></div>
        </div>
    
        <script>
            var outer = document.querySelector('.outer')
            var inner = document.querySelector('.inner')
    
            new MutationObserver(function () {
                console.log('mutate')
            }).observe(outer, {
                attributes: true,
            })
    
            function onClick() {
                console.log('click')
    
                setTimeout(function () {
                    console.log('timeout')
                }, 0)
    
                Promise.resolve().then(function () {
                    console.log('promise')
                })
    
                outer.setAttribute('data-random', Math.random())
            }
    
            inner.addEventListener('click', onClick)
            outer.addEventListener('click', onClick)
            inner.click()   // 模拟点击inner
    
        </script>
    </body>
    </html>
    
    1. 执行初始化代码, 这里与终极题1不同的地方在于: 终极题1的click是作为回调函数(dispatch), 而这里是直接同步调用的

    2. inner.click执行完毕, inner.click退栈, 由于调用栈并不为空, 所以不能轮询微任务队列, 而是继续执行run script(执行冒泡部分)
      需要注意: 由于outer.click的MutationObserver并未执行所以不会被再次添加进微任务队列中

    3. inner.click退栈, 宏任务run script执行完毕, run script也退栈 调用栈为空, 开始轮询微任务队列

    4. 调用栈、微任务队列为空, 开始轮询执行宏任务队列中的下一个任务

    5. 微任务队列、调用栈为空, 继续轮询执行宏任务队列中的下一个任务

    于是这道题最终的结果是:

    click
    click
    promise
    mutate
    promise
    timeout
    timeout
    

    参考文章:

    一次弄懂Event Loop(彻底解决此类面试问题)

    Tasks, microtasks, queues and schedules

  • 相关阅读:
    装箱、拆箱操作发生在
    @Data的注解使用以及在IDEA上安装
    Mysql中 BLOB字段转String的方法
    不属于java语言鲁棒性特点的是
    java object默认的基本方法
    哪个类可用于处理 Unicode?
    类和接口的继承
    抽象类的叙述:
    Hashtable 和 HashMap 的区别是:
    编程之美初赛第一场--焦距
  • 原文地址:https://www.cnblogs.com/fitzlovecode/p/event_loop.html
Copyright © 2011-2022 走看看