EventLoop
EventLoop (事件循环)
概念:
首先我们需要知道 JS 是一种单线程语言,简单的说就是:只有一条通道,那么在任务多的情况下,就会出现拥挤的情况,这种情况下就产生了 ‘多线程’ ,但是这种 “多线程” 是通过单线程模仿的,也就是假的。那么就产生了同步任务和异步任务。
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行” 场所”,同步的进入主线程,异步的进入 Event Table 并注册函数。
- 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue (任务队列)。
- 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的 Event Loop (事件循环)。
- 事件循环中,每进行一次循环操作称为 tick。
何为异步
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后需要执行的任务 :setTimeout
、
setInterval`; - 网络通信完成后需要执行的任务 :
XHR
、Fetch
; - 用户操作后需要执行的任务:
addEventListener
; - 如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」;
- 渲染主线程承担着极其重要的工作,无论如何都不能阻塞,因此,浏览器选择异步来解决这个问题;
- 使用异步的方式,渲染主线程永不阻塞;
面试题:如何理解 JS 的异步?
参考答案:
JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。
如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
线程与进程
线程和进程是操作系统中的两个概念:
- 进程(process):计算机已经运行的程序(程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程);
- 线程(thread):操作系统能够运行运算调度的最小单位;
听起来很抽象,我们直观一点解释:
- 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意);
- 线程:一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行程序的代码,该线程称之为主线程;
- 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程;
再用一个形象的例子解释:
- 操作系统类似于一个工厂;
- 工厂中里有很多车间,这个车间就是进程;
- 每个车间可能有一个以上的工人在工厂,这个工人就是线程;
多进程多线程开发
操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?
- 这是因为 CPU 的运算速度非常快,它可以快速的在多个进程之间迅速的切换;
- 当我们的进程中的线程获取获取到时间片时,就可以快速执行我们编写的代码;
- 对于用于来说是感受不到这种快速的切换的;
浏览器多进程
浏览器内部工作极其复杂,为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。
它主要包括以下进程:
浏览器进程:浏览器的主进程,唯一,负责创建和销毁其它进程、浏览器界面的展示、用户交互,前进后退等。
GPU 进程:用于 3D 绘制等,最多一个。
网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。
第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建。
浏览器渲染进程(浏览器内核):渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。默认情况下,浏览器会为每个标签页(Tab 页)开启一个新的渲染进程,以保证不同的标签页之间不相互影响。
每个进程中又有很多的线程,其中包括执行 JavaScript 代码的线程;
但是 JavaScript 的代码执行是在一个单独的线程中执行的;这就意味着 JavaScript 的代码,在同一个时刻只能做一件事;如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;
宏任务与微任务
宏任务是由宿主发起的,而微任务是由 JS 发起的。
名称 | 宏任务 (macrotask) | 微任务 (microtask) |
---|---|---|
谁发起的 | 宿主 (node、浏览器) | JS 引擎 |
谁先运行 | 后运行 | 先运行 |
会触发新一轮 Tick 吗 | 会 | 不会 |
事件循环中维护着两个队列
- 宏任务队列主要包括:
- script (整体代码) ps: 可以理解为外层同步代码
- ajax
- setTimeout
- setInterval
- UI 交互事件
- I/O(Node.js)
- setImmediate (Node.js 环境)
- 微任务队列主要包括:
- Promise 的 then 回调
- process.nextTick(Node.js)
- Mutation Observer API
- queueMicrotask()
宏任务与微任务是怎么执行的
main script 中的代码优先执行(编写的顶层 script 代码);
在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行;
也就是宏任务执行之前,必须保证微任务队列是空的;
如果不为空,那么就优先执行微任务队列中的任务(回调);
总结:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。(每次执行宏任务之前,都会在微任务队列里检查有没有微任务,有的话就先把微任务队列里执行完,然后再执行此个宏任务)
案例
DEMO1:
1 | setTimeout(function(){ |
分析:(其实先找同步的,然后再找微任务,再找宏任务比较快) ps:看第 2 条
- 遇到 setTimout,异步宏任务,放入宏任务队列中
- 遇到 new Promise,new Promise 在实例化的过程中所执行的代码都是同步进行的,所以输出 2
- 而 Promise.then 中注册的回调才是异步执行的,将其放入微任务队列中
- 遇到同步任务 console.log (‘5’); 输出 5;主线程中同步任务执行完
- 从微任务队列中取出任务到主线程中,输出 3、 4,微任务队列为空
- 从宏任务队列中取出任务到主线程中,输出 1,宏任务队列为空
DEMO2:
1 | setTimeout(()=>{ |
分析:
- 看到 setTimeout,为宏任务,直接不用管了,往下找
- 看到 new Promise,new Promise 在实例化的过程中所执行的代码都是同步进行的,所以输出 1
- 而 Promise.then 中注册的回调才是异步执行的,将其放入微任务队列中
- 遇到同步任务 console.log (‘2’); 输出 2;主线程中同步任务执行完
- 微任务队列中取出任务到主线程中,输出 3,此微任务中又有微任务,Promise.resolve ().then (微任务 a).then (微任务 b),将其依次放入微任务队列中
- 从微任务队列中取出任务 a 到主线程中,输出 before timeout;
- 从微任务队列中取出任务 b 到主线程中,任务 b 又注册了一个微任务 c,放入微任务队列中;
- 从微任务队列中取出任务 c 到主线程中,输出 also before timeout;微任务队列为空
- 从宏任务队列中取出任务到主线程,此任务中注册了一个微任务 d,将其放入微任务队列中,接下来遇到输出 4,宏任务队列为空
- 从微任务队列中取出任务 d 到主线程 ,输出 test,微任务队列为空
DEMO3:
1 | console.log('1'); |
分析:(大白话版)
- 同步任务直接输出 1
- 遇到 setTimeout,加入宏任务队列 (后执行的那种,整个函数就先不用看了)
- process.nextTick,加入微任务队列
- new Promise,new Promise 在实例化的过程中所执行的代码都是同步进行的,所以输出 7
- 而 Promise.then 中注册的回调才是异步执行的,将其放入微任务队列中
- 遇到 setTimeout,加入宏任务队列 (后执行的那种,整个函数就先不用看了) 主线程中同步任务执行完
- 找微任务,因为是栈,所以先入先出,所以从上往下找,process.nextTick 输出 6
- new Promise.then 输出 8
- 找宏任务;在宏任务里面先找同步代码 输出 2 、4;然后找微任务:输出 3、5
- 找宏任务;在宏任务里面先找同步代码 输出 9、11;然后找微任务:输出 10、12
DEMO4:
async await 是 promise 的语法糖
- 我们可以将 await 关键字后面执行的代码,看做是包裹在 (resolve,reject)=>{函数执行} 的代码;
- await 的下一条语句,可以看做是 then (res=>{函数执行}) 中的代码;
1 | async function async1() { |
分析:
- 遇到函数 async1,async2 定义,不会执行,不用管;
- 同步任务:输出 “script start”
- setTimeout,加入宏任务队列;
- async1 () 调用,输出 “async1 start”;下一行:await async2 (); 相当于 promise 里面的 resolve 包裹的代码,直接执行,输出”async2”;下一行 :console.log (“async1 end”); 相当于 promise 中的 then,加入微任务队列;
- 输出”promise1”,resolve () 调用.then 中的代码,但是为微任务,加入微任务队列;
- 输出”script end”
- 在微任务队列中查找,不为空,依次执行队列:输出”async1 end”;输出”promise2”
- 在宏任务队列中查找,检查微任务队列是否为空,为空,执行此个宏任务,输出”setTimeout”