(深入JavaScript 九)JavaScript在浏览器中的事件循环
进程-线程
进程:程序启动后,计算机就会开启一个或多个进程,是操作系统管理程序的一种方式。
线程:操作系统能够运行运算的最小调度单位,通常在开启一个进程中至少会有一个线程来执行程序代码,一般来说一个程序内都有一个主线程。
浏览器中运行的JavaScript
我们经常听到说,JavaScript是单线程的,这就意味着我们写的代码可能会造成我们程序的阻塞。因为单线程运行的代码,在一件事未完成的时候(比如我们写入定时器或者网络请求),那么程序会等待这段代码执行完成后才会接着执行下面的代码。但事实上,我们在JS中写了定时器或者进行了网络请求,都不会影响我们JS后续代码的执行,也就是说我们JS的主线程并没有被“卡住”。这难道说明我们的JS其实是多线程?
浏览器
虽然我们的JS代码能在浏览器中运行,并且如果我们进行了死循环的运算,浏览器就会“卡住”,但这不意味着我们的浏览器也是一个单进程、单线程的程序。目前,大多数浏览器都是多进程程序,当我们新开一个tab页的时候,浏览器就会再为这个tab页新开一个进程,这样做的目的就是为了避免一个tab页内出现了因为死循环等原因造成的页面无法响应从而影响到浏览器内所有的tab页内程序的执行。所以我们发现,如果浏览器内某个页面无法响应后我们直接就可以关闭这个页面,而其它页面不会被影响到。
并且,浏览器开启的进程并不是单独为每个tab页服务的,浏览器还需要开启其它进程来保证自己的运行,比如我们开启浏览器的调试工具,或者浏览器设置这些,都是需要别的进程执行的。而且像上面我们提到的,JS中的定时器、网络请求这些可能会十分耗时的操作也并不是拿给JS执行的,而是浏览器去执行,这样做的目的之一就是为了避免JS线程的阻塞,在执行这些耗时的操作时,我们只需要将这些操作交给浏览器的其它线程去执行,执行完毕后在特定的时间再执行对应的回调函数就行。
所以从上面我们可以明白,JS的确是单线程运行的,但是像定时器、Promise、网络请求等需要回调、耗时、异步的操作可以拿给浏览器的其它线程去执行,等执行完毕后再通过回调函数在特定的时候去执行就行。那么这一过程的具体实现是什么呢?
事件队列
在浏览器中,其实存在着一个事件队列来帮我们保存需要执行的异步函数,这个事件队列遵循着先进入队列的事件先执行的规则。而在事件队列中还分为了两个队列:
- 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
- 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等
事件循环
结合上面知识,在我们的JS代码执行时,大概会经历这样一个步骤:
- JS主线程执行,遇到定时器、网络操作、DOM操作的异步操作交给浏览器其它线程
- 定时器、网络操作、DOM操作的异步操作在浏览器其它线程处理完后进入事件队列里
- 主线程执行完后开始执行事件队列里的程序,如果这里面的程序又遇到异步操作那么开始1的操作
而这一循环的操作就组成了事件循环:
注意:其实并不是所有的异步操作都需要浏览器其它线程执行,比如Promise中then里面的代码会直接放入微任务里面,queueMicrotask函数也会直接放入微任务里面等待回调。
JS代码执行优先规则
当JS代码执行后,遵循以下规则:
- 主线程中同步代码最先执行,异步函数分别放入宏任务队列和微任务队列中,直到JS主线程中同步代码全部执行结束
- 执行任何一个宏任务之前(不是队列,是一个宏任务),比如保证微任务队列是空的。如果不为空,那么就优先执行微任务队列中的任务。
所以这里我们看段代码:
setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2");
});
});
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("then1");
});
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout2");
}, 0);
async1();
console.log("script end");
我们来分析一下(根据打印字符串来区分):
第一轮:
- 第一个setTimeout1,是异步操作,放入宏任务中等待执行
- 继续执行,遇到promise,在Promise中Promise自身的回调函数直接执行,then方法和catch方法才会进入微任务。所以控制台直接打印promise1
- async函数只声明了没调用先不管,碰到主线程中的代码script start,直接打印
- 遇到setTimeout,放入宏任务中等待执行
- 调用async1函数,在async函数中,如果没有使用await关键字,那么里面的代码执行和普通函数执行无异。如果有await关键字,那么await关键字之前的代码直接执行,直到第二个await或者函数结束的代码放入微任务中。在上面的async1函数中直接执行
console.log("async1 start") async2();
,console.log("async1 end")
放入微任务中。所以这里直接打印async1 start和async2 - 最后继续运行主线程中代码,打印script end
所以第一轮下来我们打印的代码如下:
- promise1
- script start
- async1 start
- async2
- script end
第二轮:
- 先查看微任务,因为宏任务执行前必须保证微任务队列为空。上面我们发现在微任务中还有then1和async1 end需要打印。微任务队列这时清空。
- 现在查看宏任务,遇到setTimeout1,直接打印,在里面我们还创建了一个promise,promise的then方法放入微任务中
- 现在微任务又有内容了,所以不能执行下一个宏任务,执行微任务。这里打印then2,发现又创建了一个promise,将then4放入微任务中
- 微任务有内容就不能进行下一个宏任务的执行。这里只有then4,打印then4,微任务目前清空
- 继续执行宏任务,打印setTimeout2
所以代码执行完成后打印应该为:
- promise1
- script start
- async1 start
- async2
- script end
- then1
- async1 end
- setTimeout1
- then2
- then4
- setTimeout2
转载自:https://juejin.cn/post/7242623687122272317