一、前言
JS是单线程语言,但是又可以做到异步处理高并发请求,这时就用到了JavaScript的事件循环机制
理解事件循环,可以帮助我们准确分析和运用各种异步形式,减少代码的不确定性,在一些执行效率优化上也能有明确的思路。
二、首先理解
- JS是单线程语言
- 分为 同步任务 和 异步任务
- 同步任务:立即执行的任务;在主线程上排队执行,形成一个执行栈;只有前一个任务执行完毕,才能继续执行下一个任务。
- 异步任务:不进入主线程,而进入“任务队列”的任务;只有等主线程任务全部执行完毕。“任务队列”的任务才会进入主线程执行。
- 任务队列分为 微任务队列 和 宏任务队列
- 微任务:较短时间内可以完成的任务
- 宏任务:需要相对较长时间才能完成的任务
微任务(microtask) | 宏任务(macrotask) | |
---|---|---|
谁发起的 | JS引擎 | 宿主(Node、浏览器) |
具体事件 | Promise.then()/.catch()、async await、MutaionObserver、nextTick(Node.js 环境) | script(整体代码)、setTimeout/setInterval 、UI渲染任务、事件处理器、I/O操作(如文件读取)、Ajax网络请求等异步任务 |
谁先执行 | 先执行 | 后执行 |
会触发新一轮事件循环吗 | 不会 | 会 |
- 栈:是一种
后进先出
的数据结构,数据元素在插入(即进栈)和删除(即出栈)时均从栈顶
进行操作。
类似于堆在一起的餐盘,最先放的盘子在最底下,最后放的盘子在最上面,需要把最上面的盘子一个个拿走,才能拿到最下面的盘子。 - 队列:是一种
先进先出
的数据结构,数据元素在队尾插入
而从队首删除
的。
类似于我们去排队买东西,先去的同学可以先买到。
三、灵魂三问
1. JS为什么是单线程的?
JS引擎之所以是单线程,是由于JavaScript最初是作为浏览器脚本语言开发的,并且JavaScript需要操作DOM等浏览器的API,如果多个线程同时进行DOM更新等操作则可能会出现各种问题(如竞态条件、数据难以同步、复杂的锁逻辑等),因此将JS引擎设计成单线程的形式就可以避免这些问题。
如果JS是多线程的场景描述:
现在有2个线程,process1 process2,由于是多线程的JS,所以他们对同一个dom,同时进行操作;
process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?这时可能就会出现问题了。
这样想,JS为什么被设计成单线程应该就容易理解了吧
2. 为什么需要异步? (为什么要有事件循环机制?)
如果JS中不存在异步,只能自上而下执行;如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验。所以JS中存在异步执行。
3. 单线程又是如何实现异步的呢?
异步的核心就是事件循环机制
(Event Loop)。
当 JavaScript 引擎空闲下来,也就是当前的执行栈已经清空时,JavaScript 引擎才会去查询任务队列中是否有需要执行的异步任务,这就是保证异步代码不会阻塞其他任务执行的关键。
四、什么是事件循环?
事件循环是JavaScript实现异步的一种方法,也是JavaScript的执行机制。
为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
- 先执行同步任务,如果遇到异步任务,js 引擎并不会一直等待其返回结果,而是会将这个任务挂起,继续执行执行栈中的其他同步任务。
- 当异步任务执行完毕后,再将异步任务对应的回调函数加入到一个任务队列中等待执行。
- 任务队列可以分为宏任务队列 和 微任务队列,当执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有,就将微任务队首的事件压入栈中执行。队列遵循先进先出原则。
- 当微任务队列中的任务都执行完成后,再去执行宏任务队列中的任务。
- 如果宏任务队列中有微任务,继续执行微任务。如此反复循环,直至任务队列为空。这就是JavaScript的事件循环机制。
总结JS代码执行顺序:同步任务 => 微任务 => 宏任务。
需要注意的点:
- 所有的代码都要通过函数执行栈(主线程)中调用执行。
- 等到执行栈中的task执行完之后再回去执行任务队列之中的task。
- 任务队列中存放的是回调函数。
- 执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
- 当执行一个宏任务时,如果宏任务中产生了新的微任务,这些微任务不会立刻执行,而是会被放入到当前微任务队列中,在当前宏任务执行完毕后被依次执行。
五、事件循环(Event Loop )执行顺序
- 首先执行同步代码,这属于宏任务。
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行。
- 执行所有微任务。
- 当执行完所有微任务后,如有必要会渲染页面。
- 然后开始下一轮 Event Loop,执行宏任务中的异步代码。
也就是说,一次 Event Loop 循环会处理一个 宏任务 和 所有这次循环中产生的微任务。