zoukankan      html  css  js  c++  java
  • js中的事件循环模型与特殊的定时器

    事件循环模型与定时器

    重新认识定时器

    js中有两种定时器,一种是循环定时器setInterval,一种是间隔定时器setTimeout

    setIntervalsetTimeout的不同之处在于,前者会在根据的时间间歇性执行回调函数,后者则是在设定的时间后执行一次回调函数

    平时我们在用定时器的时候,是否考虑过一个问题

    定时器真的是严格按照我们设定的时间,定时执行的吗?

    <body>
        <button id="btn">点我运行</button>
        <script>
            document.getElementById('btn').onclick = function () {
                var start = Date.now()
                setTimeout(() => {
                    var end = Date.now()
                    console.log(`定时器执行了${end - start}毫秒`)
                }, 2000)
            }
        </script>
    </body>
    

    由这个小实验可以看到,定时器并不是严格按照设定时间执行其中的回调函数的,这个时间获取小到可以忽略不计,但是它有没有可能大到严重影响我们整个js脚本,乃至页面呢? 答案是: 非常有可能!

    我们都知道js是单线程运行的,也就是一次只能进行一个任务,所以如果定时器前面有一个同步任务需要完成相当久时间,那么这个同步任务就会阻塞定时器的执行,从而造成定时器严重不准时运行

    document.getElementById('btn').onclick = function () {
        var start = Date.now()
        for (let index = 0; index < 10000000; index++) {
            var arr = new Array(1000)
        }
        setTimeout(() => {
            var end = Date.now()
            console.log(`定时器执行了${end - start}毫秒`)
        }, 2000)
    }
    

    造成这个原因涉及到了下面要介绍的一个概念: JS的事件循环模型(Event Loop)

    但是介绍这个概念之前,我们先来证明一下js是单线程运行的

    js是单线程运行的

    setTimeout(()=>{
        console.log('timeout 1111')
    }, 1000)
    setTimeout(()=>{
        console.log('timeout 3333')
    }, 3000)
    setTimeout(()=>{
        console.log('timeout 2222')
    }, 2000)
    function func () {
        console.log('func()')
    }
    func()
    console.log('alert()之前')
    alert('用于暂停主线程运行')
    console.log('alert()之后')
    

    上面这个例子是这样执行的:

    1. 页面一加载完毕,就打印输出'func()''alert()之前',马上弹出alert('用于暂停主线程运行')

    2. 接着定时器时间间隔计数根据浏览器的版本与牌子,选择阻塞计时和非阻塞计时,我的chrome是非阻塞计时,即我此时长时间不点alert弹窗的确认按钮,浏览器在背后也会自动计时定时器

    3. 之后我们点确认按钮,定时器的输出就会一起出来




    这恰恰证明了js是单线程执行的, 一次只能运行一个任务,只有等特定任务执行完后,才能执行下一个任务。

    那这特定任务指的是什么,执行完特定任务后,再执行剩下的什么呢?

    这就是下一章节介绍的事件循环模型(Event Loop)所包含的内容

    事件循环模型(Event Loop)

    我们对上面这种图中对应的概念进行标注

    首先,我们需要知道js引擎执行代码是按照一定顺序的,这就引申出js代码执行时机的问题

    根据js代码执行时机的问题,这里将代码分为两大类: 1.初始化代码    2. 回调代码

    初始化代码就是js引擎在页面初始化后马上同步执行的代码

    初始化代码有:

    • 定时器申明(内部回调函数会被分到回调代码)
    • 绑定dom事件监听
    • 发送AJAX请求

    回调代码是在特定的实际才执行的代码

    • 各种回调函数

    事件循环模型的基本运行流程

    • 先执行初始化代码(主线程中),将对应回调代码交给对应的模块管理(对应Web APIs部分)

    • 在特定时刻或回调条件触发时,对应的模块会将回调函数及其内部数据添加到回调队列中

    • 只有当所有的初始化代码执行完毕后,才会从回调队列中读取并执行其中的回调函数

    <body>
        <button id="btn">点我运行</button>
        <script>
            function test1 () {
                console.log('test1()')
            }
            test1()
    
            document.getElementById('btn').onclick = function () {
                console.log('点击了btn')
            }
    
            setTimeout(()=>{
                console.log('setTimeout()')
            }, 1000)
    
            function test2 () {
                console.log('test2()')
            }
            test2()
            /* 
                根据事件循环模型:
                    初始化代码:  1. 定义test1函数
                                2. 绑定dom事件
                                3. 定义定时器
                                4. 定义test2函数
                    
                    回调代码:    1. onclick回调函数
                                2. 定时器回调函数
            */
           
           /* 
                所以输出是:
                    'test1()'
                    'test2()'
                接着看点击事件先响应,还是定时器计数先结束
           */
        </script>
    </body>
    

    至此,我们就知道前面定时器的回调执行时机不准的原因是: 定时器指定的时间并不代表执行时间,而是将回调函数加入任务队列的时间

    事件循环模型重要的两个部分

    第一部分: 用于处理回调代码的模块,对应的是图中的WebAPIs部分,它们运行在分线程中

    这些模块并不是由JS引擎管理着,而是由浏览器进行实现、管理,这也就是为什么前面提到浏览器版本、品牌会对定时器计时有影响

    第二部分: 是回调队列, 在执行初始化代码过程中,将其中的回调代码交给对应的模块管理,当特定条件出发时,回调代码也不是立刻执行的,而是放到回调队列中, 因为要等初始化代码全部执行完,才能执行回调函数, 所以回调队列起到的是一个缓冲的作用

    到这,事件循环模型的知识就总结的较为清晰了,但是还有一个地方需要补充

    回调代码(异步任务)中,各种异步任务之间是否也有优先级呢? 来看看这个例子

    setTimeout(()=>{
        console.log('timeout')
    },0)
    let p = Promise.resolve('promise')
    p.then(
        result => {
            console.log(result)
        }
    )
    
    /* 
        运行结果:
            'promise'
            'timeout'
    */
    

    就会看到promise异步回调是会比setTimeout定时器回调先执行的,这说明各种异步任务之间也是有优先级之分(先后执行顺序)

    哪异步任务的优先级是如何区分,就是下章节所要介绍的知识

    宏任务与微任务

    参考于博主听风是风的《JS执行机制详解,定时器时间间隔的真正含义》

    JavaScript引擎将整代码所对应的任务整体分为宏任务微任务

    宏任务有:

    • script环境
    • 定时器setTimeout、setInterval
    • I/O
    • 事件
    • postMessage
    • MessageChannel
    • setImmediate (Node.js)

    微任务有:

    • Promise
    • process.nextTick
    • MutaionObserver

    根据上面的例子,我们就能知道,微任务的优先级高于宏任务

    这里着重需要注意的是: script环境也属于宏任务,所以上来就执行的同步代码是属于宏任务的

    那前面说的,微任务的优先级高于宏任务,但是宏任务的同步代码又是首先执行。这不就冲突、乱套了吗

    所以比较准确的说法应该是: 除了宏任务中同步执行的初始化代码,微任务的优先级是高于宏任务的

    用流程图表示一下

    最后通过一道题目来展现巩固一下这个过程

    const P1 = function () {
        return new Promise((resolve) => {
            console.log('p1')
            setTimeout(() => {
                resolve()
            })
        })
    }
    const P2 = function () {
        return new Promise((resolve) => {
            console.log('p2')
            resolve()
        })
    }
    
    setTimeout(() => {
        console.log('s1')
        P1().then(() => {
            console.log(1)
        })
    })
    setTimeout(() => {
        console.log('s2')
        P2().then(() => {
            console.log(2)
        })
    })
    
    /* 
        最后输出是:
        s1
        p1
        s2
        p2
        2
        1
    */
    

    图片配合文字解析

    第1步: 初始化代码

    第2步: 将代码中的回调代码(异步回调)交给对应模块处理

    这里比较容易误会的地方是,为什么只交定时器的两个回调。

    因为P1,P2这两个是分别要等这两个定时器的回调执行才会执行,现在P1,P2的一切都还处于完成初始化代码的状态

    第3步: 此时初始化代码(同步代码)所有都执行完了,开始轮询任务队列中的回调代码(异步代码)

    此时任务队列的状态

    第4步: 轮询执行第一个回调函数,也就是setTimeout1, setTimeout1的又会开启它事件循环

    第5步: setTimeout1的事件循环中,由于console.log('s1')P1这些都是初始化代码(同步代码), 所以先输出打印s1

    第6步: 然后执行P1(), P1又开始P1的事件循环,同步代码console.log('p1') 输出打印p1,同步代码执行完毕,将其中的回调代码(P1中的定时器)放入任务队列中,P1的事件循环结束

    此时任务队列的状态

    第7步: 将setTimeout1中还有then回调经对应模块处理后,放入任务队列,setTimeout1整个回调执行完成

    此时任务队列的状态

    第8步: 接着轮询执行setTimeout2,开始setTimeout2的事件循环

    第9步: setTimeout2的事件循环中,同步代码console.log('s2') 输出打印s2

    第10步: 然后执行P2(), P2又开始P2的事件循环,同步代码console.log('p2')resolve() 输出打印p2,然后改变Promise状态,同步代码执行完毕,没有回调代码,所以P2的事件循环结束

    此时任务队列的状态

    按理说根据队列的特性,此时我们应该轮询执行P1中定时器的回调函数,但是别忘记了定时器是宏任务,promise是微任务,微任务的优先级比宏任务高

    所以,改变一下任务队列

    第10步: 由于Promise的状态已被改变,setTimeout2中的then回调执行,打印输出2

    此时任务队列的状态

    第11步: 轮询执行P1中的定时器回调,改变P1的Promise的状态

    第12步: 由于Promise的状态已被改变,setTimeout1中的then回调执行,输出1

    写在最后

    这篇笔记,是我写的最没把握的一篇,因为总结到最后,我感觉有太多的地方我无法理解,甚至很可能理解的也是错误的

    不管怎样先记录下来,等到时真正明白了或意识到什么地方错了再回来修改

    十分欢迎读者提出其中的错误

  • 相关阅读:
    【并查集】亲戚
    【图论】Car的旅行线路 NOIP 2001
    【贪心】排座椅
    【DP】花店橱窗布置
    【NOIP】NOIP考纲总结+NOIP考前经验谈
    【NOIP】考前须知
    NOIP 2016 PJ T4 魔法阵
    NOIP 2016 PJ T3 海港
    【高精度】麦森数 NOIP 2003
    【带权并查集】食物链 NOIP 2001
  • 原文地址:https://www.cnblogs.com/fitzlovecode/p/jsadvanced13.html
Copyright © 2011-2022 走看看