美文网首页
从一个实例看完整的事件循环过程

从一个实例看完整的事件循环过程

作者: 1海内无双1 | 来源:发表于2018-06-27 11:48 被阅读0次

一个完整的事件循环过程大概分为以下几步:

1.检查调用栈是否为空,如果不为空则等待调用栈执行完毕,为空则检查事件队列是否为空;如果事件队列为空则不执行任何操作,不为空则将队首的事件处理器压入执行栈执行;

2.调用栈中的代码执行完毕后检查微任务队列是否存在微任务,如果有则执行微任务,并且按顺讯执行完所有的微任务;

3.执行完所有的微任务后,进行判断是否要进行UI渲染,如果需要则进行UI渲染不需要则回到步骤1,如此就完成了一个事件循环;

如题通过一个实例来研究这个过程:

    <button id="clickMe">click me!</button>
    <script>
        // 获取一个button元素
        var clickMe = document.getElementById("clickMe");
        // 定义一个延迟函数
        function delay(second) {
            console.time('延迟代码执行时间');
            for (i = 0; i < 10000000000; i++) {
                if (i == 379999999*second) { // 379999999大概为当前浏览器环境下执行1秒能循环的次数
                    break;
                }
            }
            console.timeEnd('延迟代码执行时间');   
        }
        // 为button添加事件监听
        clickMe.addEventListener("click",function(){
            console.log("触发了clickMe");
            clickMe.innerHTML = "重新设置按钮内容";
            delay(2);
            setTimeout(function(){
                console.log("触发了setTimeout");
                delay(2);
                console.log("完成了setTimeout");
            },0);
            Promise.resolve().then(function(){
                console.log("触发了Promise")
                delay(2);
                console.log("完成了Promise"); // 此处可以打上断点观察
            });
        })
    </script>

代码分析:

前提:JavaScript是单线程的,这说明我们想要同一时间不可能执行两个函数,同样也不可能执行两个事件;如果触发多个事件就会保存在一个事件队列(task queue)里按顺序调用事件的处理器函数;执行处理器函数实在调用栈中(call stack),调用栈到底长什么样我们会在下面看到。首先我们写个一个按钮id为clickMe(正如它的内容),然后是脚本内容,包含一个指向dom元素的变量clickMe和一个延迟函数delay以及调用了一个绑定事件处理器的方法;

1.某一个时刻点击了clickMe,绑定在clickMe上的事件处理器被添加到了事件队列,需要提醒的是事件队列的操作跟调用栈执行并非同一线程,因为不如此的话在调用栈工作时产生的事件将无法添加到队列,我们知道这显然不是的。按照步骤1调用栈不存在可执行代码,随即检查事件队列是否为空,此时事件队列恰有我们触发的按钮点击事件的处理器函数,随即将处理器函数压入到调用栈中执行,然后按照从上到下的顺序执行代码,首先在控制台可以看到“触发了clickMe”,然后更改了按钮的html内容,内容并不会马上被渲染还是因为单线程,在代码执行时渲染引擎并不会执行渲染(当然并不代表就共用一个线程),然后又定义了一个超时器,当然回调函数会在一个“合适”的时机执行,然后是触发了一个promise,同样超时器的回调函数也会在一个合适的”时机“执行,此时快照如下:

1.png

2.当代码执行到了处理器函数末尾时,函数执行完毕出栈,开始下个步骤,检查是否存在微任务,微任务(micro task)是相对于宏任务(macro task)的,宏任务包含:主程序script,setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering,这些任务被分配到浏览器的不同部分去单独执行所以定义为宏任务,微任务包含:process.nextTick, Promise, Object.observe, MutationObserver;这些任务不需要单独执行而又有别于同步执行称为微任务;现在promise是微任务,setTimeout是宏任务,当检查微任务队时,我们知道promise的“时机”来了,随即将回调函数压入调用栈执行,首先我们会看到:“触发了promise”,大概经过两秒后我们又看到“完成了promise”,那你会问延迟函数的作用是什么,很简单方便截图,截图的作用呢?很简单验证事件处理器中修改了按钮的内容是否改变也就是是否发生了UI渲染,那打个断点不就行了还得费劲写个函数?嗯。。。看下面第二张图,将断点打在 “console.log("完成了Promise");” 观察按钮上的文字,对,它改变了!!!在单线程的JS函数执行时还能进行UI渲染?显然这是浏览器debugger策略,便于我们观察效果而已,但对我们研究真正过程会产生误解。执行到延迟函数时快照如下:

2.png

button的内容并没有改变,证明了在promise执行完之前不会渲染UI

7.png

3.promise执行完毕后开始进行UI渲染,渲染后快照如下图所示(在setTimeout得延迟函数中截图):

3.png

4.到第3步已经是一个完整的循环了,但我们仍旧写了一个定期器来验证微任务比宏任务执行的早并且在渲染之前执行,重复这个过程将定时器回调压入调用栈执行,我们借用延迟函数获得如下快照:

4.png

总结:理清事件循环的过程有助于我们更准确的知道我们的代码是如何执行的,当出现问题或者写代码之初就避免这样的问题,本文从一个实例出发分析了事件循环过程,并且也学习到浏览器并非单线程的,一个循环可能需要多个部分分工合作,以v8为代表的现代浏览器JS引擎在性能上的优化非常好,但这依旧不能成为我们写出不合理代码的理由,当然这个实例包含的情况并不丰富,有些问题并没有证明,例如:是否所有的微任务都会被执行完才开始渲染?浏览器是怎么判定需不需要渲染的?如果事件处理器函数不存在或者事件监听没有添加之前这个事件被触发了浏览器是怎么做的?这些问题就留给大家自己搞清楚吧。

相关文章

  • 从一个实例看完整的事件循环过程

    一个完整的事件循环过程大概分为以下几步: 1.检查调用栈是否为空,如果不为空则等待调用栈执行完毕,为空则检查事件队...

  • Spring之循环依赖

    Spring在Bean的实例化过程中,提供了对循环依赖的解决方案,但是这部分代码非常的生涩难懂,今天,我们就从一个...

  • 学习 nodejs I /O 交互

    1 事件循环 Node的执行模型实际上是事件循环。在进程启动时,Node会创建一个无限循环,每一次执行循环体的过程...

  • 《浏览器工作原理与实践》学习笔记(四)

    消息队列和事件循环 要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。 事件循环机制:相比于线性...

  • 事件的完整处理过程

    事件的完整处理过程 1.先将事件对象由上到下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件 2.调用...

  • 事件传递的完整过程

  • 知识点

    ...语法 反转 Event Loop(事件循环) 主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个...

  • Vue - 购物车案例 - 创建Vue实例

    案例主要部分: 创建一个完整的Vue实例 html: 我们在js文件中创建vue实例:这是一个完整的Vue实例: ...

  • 浏览器 Event Loop

    导语 本文的内容是浏览器的事件循环,并不是 nodejs 的事件循环,不要将两者混淆。 我们先从一段代码开始: 这...

  • 2.Vue实例生命周期

    Vue实例有一个完整的生命周期,顾名思义就是指Vue实例从创建到销毁的过程。下面将借助官网上的Vue实例生命周期示...

网友评论

      本文标题:从一个实例看完整的事件循环过程

      本文链接:https://www.haomeiwen.com/subject/nucbyftx.html