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”