JavaScript的事件循环机制学习总结(宏任务微任务)

JS事件循环机制即使你不知道,理论上讲也不影响写代码,但是你知道了的话会更加清楚的明白为什么要这么做,同时也为你更加深入的学习打下基础,最关键的是面试火箭型题目爱问。在查阅了几个大佬的文章后对这个问题也有了一定的认识,因此总结一下,力求采用全面且通俗易懂的方式让大家明白。


目录

1.线程与进程

2.浏览器内核(浏览器渲染进程)

3.事件循环机制

4.宏任务队列与微任务队列

5.示例代码:

6.事件循环机制总结

7.参考内容


1.线程与进程

启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序的资源的整合。这些资源有“虚地址空间”、“文件句柄”、“程序运行的其他东西”等等。

在进程内部,系统会创建一个称为线程(执行线程)的内核对象,他代表真正执行的程序。

进程:系统分配的独立资源,是CPU资源分配的基本单位,进程是由一个或多个线程组成的。浏览器通常就是多进程的。

线程:线程是进程的执行流,是CPU调度和执行的基本单位,同个进程中的多个线程共享该进程的资源。

关于线程,要知道以下几点:

①默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束。

②线程可以派生其他线程,因此在任意时刻,一个进程都可能包含不同状态的多个线程,他们执行程序的不同部分。

③如果一个进程有多个线程,他们共享进程的资源。

④系统处理器CPU执行所调度的单元是线程,不是进程。

2.浏览器内核(浏览器渲染进程)

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。

3.事件循环机制

JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程(浏览器内核)是提供多个线程的。

JavaScript 事件循环机制分为浏览器和 Node 事件循环机制,两者的实现技术不一样,浏览器 Event Loop 是 HTML 中定义的规范,Node Event Loop 是由 libuv 库实现。这里主要指浏览器部分。

JavaScript 引擎是单线程的,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。HTML5 中提出了 Web-Worker API,主要是为了解决页面阻塞问题,但是并没有改变 JavaScript 是单线程的本质。


JavaScript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被先后放到调用栈中等待主线程执行。

JS引擎线程会维护一个 执行栈(调用栈),同步代码会依次加入执行栈(调用栈)然后执行,结束会退出执行栈

如果执行栈里的任务执行完成,即执行栈(调用栈)为空的时候(即JS引擎线程空闲),事件触发线程才会从任务队列取出一个任务放入执行栈(调用栈)中执行。
 

JS事件循环简图:

JS处理同步任务和异步任务的流程图:

  • JS 调用栈

    JS 调用栈是一种后进先出的数据结构当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。

  • 同步任务、异步任务

    JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构

  • 事件循环Event Loop

    调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候事件触发线程就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。


再来看一个详细的事件循环示例图,流程如下:

step1:主线程读取JS代码,此时为同步环境,形成相应的堆和调用/执行栈;

step2:  主线程遇到异步任务,交给相应的线程单独去维护异步任务;

step3:  等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到任务队列中,任务队列中的回调函数将等待被执行;

step4: 主线程执行完毕,栈被清空,查询任务队列,如果存在任务,则取出一个任务推入主线程处理(任务队列先进先出);

step5: 重复执行step2、3、4;称为事件循环。

当遇到定时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他任务,这样便实现了异步非阻塞。

定时器触发线程也只是为 setTimeout(..., 1000) 定时而已,时间一到,还会把它对应的回调函数(callback)交给 任务队列 去维护,JS引擎线程会在适当的时候去任务队列取出任务并执行。

4.宏任务队列与微任务队列

其实事件循环机制任务队列的维护是由事件触发线程控制的。事件触发线程同样是浏览器渲染引擎提供的,它会维护一个“任务队列”。

JS引擎线程会维护一个 执行栈(调用栈),同步代码会依次加入执行栈(调用栈)然后执行,结束会退出执行栈

如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从任务队列取出一个任务(即异步的回调函数)放入执行栈中执行。

如上示意图,任务队列存在多个,同一任务队列内,按队列顺序被主线程取走;不同任务队列之间,存在着优先级,优先级高的优先获取(如用户I/O);

其中任务队列又可以细分为两种类型,一种为微任务队列(microtask queue),另一种为宏任务队列(macrotask queue)

微任务队列(microtask queue):唯一,整个事件循环当中,仅存在一个;同一个事件循环中的microtask会按队列顺序,串行全部执行完毕为止;

宏任务队列(macrotask queue):不唯一,存在一定的优先级(用户I/O部分优先级更高);同一事件循环中,只执行一个。

常见的宏任务和微任务:

宏任务(macrotask)包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

微任务(microtask)包括:process.nextTick, Promises, Object.observe, MutationObserver。

图中所列出的任务队列均为宏任务队列,而ES6 的 promise产生的异步任务队列为微任务队列。

进一步将微任务microtask加入到JS运行机制流程中,则应该是:

step1:主线程读取JS代码,此时为同步环境,形成相应的堆和调用/执行栈;

step2:  主线程遇到异步任务,交给相应的线程单独去维护异步任务;

step3:  等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到宏/微任务队列中,随后等待被主线程调用栈执行;

step4:主线程调用栈查询微任务队列,将其按序执行,全部执行完毕;

step5:主线调用栈程按顺序查询一个宏任务队列执行;

step6:重复step4、step5。

微任务队列中的所有callback处在同一个事件循环中(因为微任务就一个),而宏任务队列中的callback有自己的事件循环(宏任务队列数量不唯一)。

简而言之:同步环境执行 -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个) -> 事件循环1(microtask queue的All)-> 事件循环3(macrotask queue中的一个).............................

利用微任务可以形成一个同步执行的环境,但如果微任务队列太长,将导致宏任务队列长时间执行不了,最终导致用户I/O无响应等,所以使用需慎重。

5.示例代码:

console.log(1);
setTimeout(function() {
    console.log(2);
})
let promise = new Promise(function(resolve, reject) {
    console.log(3);
    resolve();
})
promise.then(function() {
    console.log(4);
})
console.log(5);

示例中,setTimeout 和 Promise被称为任务源,来自不同的任务源注册的回调函数会被放入到不同的任务队列中。

有了宏任务和微任务的概念后,那 JS 的执行顺序是怎样的?是宏任务先还是微任务先?

第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中微任务队列有无要执行的任务,若存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取另一个宏任务队列中的任务执行,执行完毕后再检测本次循环中微任务队列有无要执行的任务,若存在的话就依次从微任务的任务队列中读取执行完所有的微任务,如此循环。

详细执行结果顺序分析:

1.上面的示例中,第一次事件循环,整段代码作为宏任务经调用栈进入主线程执行。

2.遇到了 setTimeout ,交给定时器线程,等到指定的时间后将回调函数放入到宏任务的任务队列中。

3.遇到 Promise,其本身为同步主线程内容,将 then 后函数经异步线程处理后放入到微任务的任务队列中。

4.主线程同步内容执行完毕后,调用栈清空。此时调用栈会去检测微任务的任务队列中是否存在任务,存在就全部执行,不存在就找下一个宏任务。因此第一次的循环结果打印为: 1,3,5,4。

5.接着再到宏任务的任务队列中按顺序取出一个宏任务到调用栈中让主线程执行,那么在这次循环中的宏任务就是 setTimeout 注册的回调函数,执行完这个回调函数,发现在这次循环中并不存在微任务,就准备进行下一次事件循环。

6.检测到若干宏任务队列中已经没有了要执行的宏任务队列了,那么就结束事件循环。最终的结果就是 1,3,5,4,2。

6.事件循环机制总结

1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2.遇到耗时或者容易阻塞UI的异步任务,JS引擎会将它们直接交给 webapi,就是其他浏览器内核中其他线程处理,处理完毕或有了结果后,异步任务会被推入到任务队列中(究竟是宏任务队列还是微任务队列请看上面的分类--JS作者认为规定的)。
3. 主线程执行栈之外,还存在一个"任务队列"(task queue)。一但执行栈(调用栈)中的所有同步任务执行完毕,执行栈为空,此时调用栈去唯一的微任务队列中查看有没有微任务可以执行 ,若没有就找下一个宏任务队列中的宏任务执行即进入下一个事件循环(总之:同步优先级>微任务优先级>宏任务优先级)。
4. 只要执行栈(调用栈)空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复,这种机制就被称为事件循环(event loop)机制。

7.参考内容

https://www.bilibili.com/video/BV1K4411D7Jb

https://www.bilibili.com/video/BV1kf4y1U7Ln/?spm_id_from=trigger_reload

https://www.cnblogs.com/itgezhu/p/13259966.html

https://www.cnblogs.com/yqx0605xi/p/9267827.html

https://blog.csdn.net/qq_39207948/article/details/81671304

https://blog.csdn.net/qq_31628337/article/details/71056294?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

《C#图解教程(第5版)》

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

韦_恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值