十四、深入核心,详解事件循环机制【上】

简介: JavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直在试图寻找一条核心的线索,只要顺着这条线索,我就能够一点一点的进步。前端基础进阶正是围绕这条线索慢慢展开,而事件循环机制(Event Loop),则是这条线索的最关键的知识点。

十四、深入核心,详解事件循环机制【上】


JavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直在试图寻找一条核心的线索,只要顺着这条线索,我就能够一点一点的进步。

前端基础进阶正是围绕这条线索慢慢展开,而事件循环机制(Event Loop),则是这条线索的最关键的知识点。


所以,我就马不停蹄的去深入的学习了事件循环机制,并总结出了这篇文章跟大家分享。

事件循环机制从整体上告诉了我们JavaScript代码的执行顺序。但是在我学习的过程中,找到的许多国内博客文章对于它的讲解浅尝辄止,不得其法,很多文章在图中画个圈就表示循环了,看了之后也没感觉明白了多少。但是他又如此重要,以致于当我想要面试中高级岗位时,事件循环机制总是绕不开的话题。特别是ES6中正式加入了Promise对象之后,对于新标准中事件循环机制的理解就变得更加重要。这就很尴尬了。


最近有两篇比较火的文章也表达了这个问题的重要性。

这个前端面试在搞事[1]80% 应聘者都不及格的 JS 面试题[2]

但很遗憾的是,大神们告诉了大家这个知识点很重要,却并没有告诉大家为什么会这样。所以当我们在面试遇到这样的问题时,就算你知道了结果,面试官再进一步问一下,我们依然懵逼。


学习事件循环机制之前,我默认你已经懂得了如下概念


执行上下文(Execution context)


函数调用栈(call stack)


队列数据结构(queue)


Promise


(我会在下一篇文章专门总结Promise的详细使用)


因为chrome浏览器中新标准中的事件循环机制与nodejs类似,因此此处就整合nodejs一起来理解,其中会介绍到几个nodejs有,但是浏览器中没有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate


OK,那我就先抛出结论,然后以例子与图示详细给大家演示事件循环机制。


我们知道JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。


当然新标准中的web worker涉及到了多线程,这里就不讨论了。


JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。


微信图片_20220510235150.png


一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。


任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。


macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。


micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)


setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。


// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
    console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行


来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。


事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。


其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。纯文字表述确实有点干涩,因此,这里我们通过2个例子,来逐步理解事件循环的具体顺序。


// demo01  出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。
// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
    console.log('timeout1');
})
new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})
console.log('global1');


首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。


微信图片_20220510235153.jpg


第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。


setTimeout(function() {
    console.log('timeout1');
})


微信图片_20220510235156.jpg


第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。


因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。


微信图片_20220510235156.jpg

微信图片_20220510235159.jpg

微信图片_20220510235202.jpg


script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。


第四步:第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。



微信图片_20220510235205.jpg


第五步:当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。


微信图片_20220510235208.jpg


这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。


微信图片_20220510235212.jpg


这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。

那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。


这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复杂一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。


相关文章
|
2月前
|
安全 数据处理 C++
【Qt 底层之事件驱动系统】深入理解 Qt 事件机制:主事件循环与工作线程的交互探究,包括 QML 的视角
【Qt 底层之事件驱动系统】深入理解 Qt 事件机制:主事件循环与工作线程的交互探究,包括 QML 的视角
454 3
|
2月前
|
前端开发 JavaScript UED
JavaScript 的事件循环机制是其非阻塞 I/O 模型的核心
【5月更文挑战第9天】JavaScript的事件循环机制是其非阻塞I/O的关键,通过单线程的调用栈和任务队列处理异步任务。当调用栈空时,事件循环从任务队列取出一个任务执行,形成循环。异步操作完成后,回调函数进入任务队列,等待被事件循环处理。微任务如Promise回调在每个宏任务结束后执行。此机制确保JavaScript能高效处理异步操作,不阻塞主线程,提供流畅的用户体验。
26 2
|
2月前
|
Java
Java线程通信的精髓:解析通知等待机制的工作原理
Java线程通信的精髓:解析通知等待机制的工作原理
32 3
Java线程通信的精髓:解析通知等待机制的工作原理
|
2月前
|
存储 关系型数据库 MySQL
纯c协程框架NtyCo实现与原理
纯c协程框架NtyCo实现与原理
84 1
|
2月前
|
存储 调度
FreeRTOS深入教程(队列内部机制和源码分析)
FreeRTOS深入教程(队列内部机制和源码分析)
81 0
|
9月前
|
Dart
带你读《深入浅出Dart》十六、事件循环和协程机制(2)
带你读《深入浅出Dart》十六、事件循环和协程机制(2)
|
9月前
|
Dart
带你读《深入浅出Dart》十六、事件循环和协程机制(5)
带你读《深入浅出Dart》十六、事件循环和协程机制(5)
|
9月前
|
Dart
带你读《深入浅出Dart》十六、事件循环和协程机制(3)
带你读《深入浅出Dart》十六、事件循环和协程机制(3)
|
9月前
|
Dart
带你读《深入浅出Dart》十六、事件循环和协程机制(4)
带你读《深入浅出Dart》十六、事件循环和协程机制(4)
|
9月前
|
Dart JavaScript 前端开发
带你读《深入浅出Dart》十六、事件循环和协程机制(1)
带你读《深入浅出Dart》十六、事件循环和协程机制(1)