事件循环机制
JavaScript是一门单线程,非阻塞的脚本语言
单线程: 只有一条主线程来处理所有的任务
非阻塞:当代码需要进行异步任务的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定的规则去执行相应的回调
那么非阻塞是在js引擎中是怎么实现的呢?——event loop(事件循环机制)
浏览器环境下js引擎的事件循环机制
1.执行栈与事件队列
变量的存储位置:
- 堆:存放一些对象。
- 栈:存放一些基础变量以及对象的指针。
执行上下文(context):
当我们调用一个方法的时候,js会生成一个与这个方法相对应的执行环境,这个执行环境就是执行上下文,执行上下文存在这个方法的私有作用域
!执行栈:
**在js中,当一系列方法被一次调用的时候,因为js是单线程的,同一时间只能执行一个任务,于是这些方法会被排队在一个单独的地方**,这个地方就是执行栈。
**当一个js脚本第一次执行的时候,js引擎会解析这段代码,<u>并将其中的*同步代码*按照执行顺序加入执行栈中,然后从头开始执行</u>。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕**。
要注意的是,上面已经提到当一个方法执行的时候会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。
以上的过程说的都是<u>同步代码</u>的执行。**
那么当一个异步代码(如发送ajax请求数据)执行后会如何呢?前文提过,js的另一大特点是非阻塞,实现这一点的关键在于下面要说的这项机制——事件队列(Task Queue)。
说白了就是,js的非阻塞特点是通过事件队列(Task Queue)实现的
!事件队列:
当js引擎遇到一个异步事件后并不会一直等待其返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务,当一个异步事件返回结果后,js会将这个事件加入到与当前执行栈不同的另一份队列,我们称之为事件队列(也有叫异步队列的)
!事件循环(Event Loop):
**被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务(同步任务)都执行完毕, 主线程处于闲置状态时,主线程才会去查找事件队列是否有任务。如果有,那么主线程会从事件队列中取出排在第一位的事件任务,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码,如果里面存在异步代码,则把里面的异步代码挂到事件队列里,如此反复,这样就形成了一个无限的循环。**这就是这个过程被称为“事件循环(Event Loop)”的原因。
这里还有一张图来展示这个过程:
图中的stack表示我们所说的执行栈,web apis则是代表一些异步事件,而callback queue即事件队列。
在事件队列中,异步任务又分两类,<u>宏任务和微观任务</u>,执行优先级是微任务>宏任务**
2.宏任务和微任务
宏任务(macrotask )和微任务(microtask)
宏任务一般是:包括整体代码script中的代码
,setTimeout()
,setInterval()
、I/O
、UI render
。
微任务主要是:Promise.then()
、Object.observe()
、MutationObserver()
。
事件队列的工作流程:
**在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环**
***<u>换一种解释就是</u>***
在挂起任务时,`JS 引擎`会将所有任务按照类别分到这两个队列中,开始执行事件队列中的任务时,首先判断是否有微任务存在,有微任务则先取出微任务中的首个任务到执行栈中执行,执行完毕后在回去判断事件队列中是否还有微任务,判断事件队列不存在微任务后,再执行事件队列中的宏任务,在宏任务的队列中取出第一个任务,将它放到执行栈中执行,如果宏任务中还有异步操作,则将这些异步操作接着挂载到事件队列中,当该宏任务在执行栈中结束后,再去事件队列中判断是否有微任务,有则先处理微任务,没有则再取宏任务队列中最前面的任务,放到执行栈中执行,周而复始,直至两个队列的任务都取完。
我们只需记住**当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。<u>同一次事件循环中,微任务永远在宏任务之前执行</u>**。
宏任务与微任务关系图: