参考 一篇搞定(Js异步、事件循环与消息队列、微任务与宏任务)
浏览器中的单线程与多线程
JavaScript是一门单线程、异步、非阻塞、解析类型脚本语言。
JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程。但是浏览器的渲染进程是提供多个线程的,如下:
- JS引擎线程(渲染解析JS的)
- 定时器监听等线程
- HTTP网络线程
- DOM事件监听触发线程
- GUI渲染线程
当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout
计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他同步任务,这样便实现了 异步非阻塞。
JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等…),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到消息队列(事件队列)中,消息队列中的回调函数等待事件循环机制进行查询并被执行。
总结:
浏览器是多线程的,但是js的引擎是单线程的,js在同步执行任务的时候,如果遇到异步任务,就会交给别的线程去处理,等待异步任务触发并有了运行结果的时候,把回调函数当作一个任务加入到事件队列中,然后等待js主线程同步任务完成,再依据事件循环机制从事件队列中拿出异步任务进行执行。
那么事件队列机制和事件循环这两个机制具体是如何运作的呢?
事件队列和事件循环
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
- 一但”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,查找里面的事件与对应的异步任务,如果有异步任务,就结束等待状态,将异步任务放入执行栈,开始执行。
- 主线程不断重复上面的第三步。
以上就是事件队列和事件循环机制的大概过程。在这个过程中,异步任务分为异步宏任务和异步微任务
异步宏任务(macrotask)
- 定时器(setTimeout、setInterval)
- DOM事件
- HTTP请求(ajax、fetch、jsonp…)
异步微任务(microtask )
- promise(resolve/reject/then…)
- async await
- requestAnimationFrame
- process。nextTick(node中process。nextTick的优先级高于Promise)
事件循环执行机制具体是这样的:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)
总结:js主线程最开始的同步代码也算是宏任务,在执行宏任务时遇到Promise等,会创建微任务(例如.then()
里面的回调),并加入到微任务队列队尾。某一个宏任务执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有微任务都执行完毕,然后开启下一个宏任务。这就是事件循环机制。
举例子说明事件队列和事件循环机制
setTimeout(function () {
console.log(1);
}, 1000);
console.log(2);
new Promise(resolve => {
console.log(3);
resolve();
console.log(4);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
});
console.log(7);
const fn = () => {
console.log(9);
};
(async function () {
console.log(8);
await fn();
console.log(10);
await fn();
console.log(11);
})();
console.log(12);
复制代码
一步一步地说
setTimeout(function () {
console.log(1);
}, 1000);
复制代码
浏览器会设置一个宏任务,我们把它叫做任务1.开始计时,到达1000ms后,通知主线程把回调函数执行。浏览器会把这个任务1放到异步宏任务队列中排队等待,并开启一个监听线程,用来计时。
console.log(2);
复制代码
立即打印 2
,同步执行
new Promise(resolve => {
console.log(3);
resolve();
console.log(4);
})
复制代码
new Promise
的时候会立即执行回调函数executor
,所以会输出3
。resolve()
会立即把promise实例的状态改为成功'fulfilled'
,同时在微任务队列中创建一个任务,我们叫他任务2:能够把后期基于then
存放进来的方法onfulfilledCallback
通知浏览器执行(前提:方法还没有被执行过)- 然后输出
4
以上几步图示:
.then(() => {
console.log(5);
})
复制代码
这里 .then
中的回调也是一个微任务,假设是任务3,因为上面的任务2直接是同步的 resolve()
,所以任务2不会被放到为微任务队列中,会跳过,而是把这个确定状态的任务3放到微任务队列
.then(() => {
console.log(6);
});
复制代码
这里 .then
中的回调也是一个微任务,假设是任务4,这里不会立刻把这个微任务放到微任务队列中,因为上面的微任务3还没有执行,可能不知道其状态,所以属于暂存的状态,什么时候等上面为任务3执行了,确定了那个 promise
的状态,才决定是否执行这个微任务
console.log(7);
复制代码
直接输出7
上面这几步对应的图例:
const fn = () => {
console.log(9);
};
复制代码
声明函数
console.log(8);
await fn();
复制代码
遇到立即执行函数,打印8
遇到 await
:
其实 await
可以写成promise的样子,就把它当作promise理解即可,所以这里相当于把当前上下文下中,第一个await
下面所有的代码都当成回调了。所以:
立即把 fn
执行,相当于执行promise中的executor,这里默认是resolve的,所以在当前上下文下,会把 await
下面的所有代码当成一个新的异步微任务(任务5),放到任务队列中。类似于任务3中的.then
中的回调
所以现在微任务队列中只有任务3,任务5。
所以下面的代码先不会执行,继续往下执行
console.log(12);
复制代码
输出 12
到这里,同步任务都执行完了,主线程空闲下来了,开始执行异步任务。这里因为js是单线程的,如果同步任务没有执行完,也就是主线程没有空闲下来,不论异步任务是否到达了可执行的阶段(例如 setTimeout(()=>{},0)
),都不能执行。
这一段过程的图示如下
接下来进入到异步队列,开始执行异步任务。
过程是这样的:
-
因为在刚才最开始的同步运行中,任务2直接resolve,所以任务2 会跳过,直接把任务3加到微任务队列中。因为任务3还没运行,任务4是否要加到微任务队列中,要看任务3运行后的状态。下面的fn运行完,
resolve
了,也是一样的道理,第一个await
下面所有的代码是任务5,都会放到第一轮微任务队列中,执行时,也会和任务3一样直接执行。那么事件循环机制第一次循环要执行的任务按顺序就是任务3,任务5,所以输出5,10,9(第二个fn)。.then
和await
其实本质一样 -
执行任务3时,又会有一个新任务,任务4,是接下来的
then
。执行任务5的时候,会又遇到await
,所以之后的代码又创建了任务6 -
所以第二次事件循环机制,就会执行任务4,任务6,分别输出6,11
-
最后执行宏任务,输出1
这样每次进行去事件队列中拿任务,然后执行,执行完之后再去事件队列中查询,这就是事件循环机制