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

    写在最后

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

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

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

  • 相关阅读:
    POJ 1003 解题报告
    POJ 1004 解题报告
    POJ-1002 解题报告
    vi--文本编辑常用快捷键之光标移动
    常用图表工具
    September 05th 2017 Week 36th Tuesday
    September 04th 2017 Week 36th Monday
    September 03rd 2017 Week 36th Sunday
    September 02nd 2017 Week 35th Saturday
    September 01st 2017 Week 35th Friday
  • 原文地址:https://www.cnblogs.com/fitzlovecode/p/jsadvanced13.html
Copyright © 2011-2022 走看看