络腮胡菲菲 发表于 2024-11-8 10:49:02

浏览器中的事件循环

1. 浏览器进程和线程

浏览器是多进程的,每当你打开一个 Tab ,都会有一个进程被体系创建出来。 这就是为什么你
会在任务管理器中瞥见多个 chrome.exe 的缘故起因。
每一个 tab 都是一个浏览器『渲染进程』。 当然,除了一个( 或多个 )渲染进程,浏览器还有其它
的进程,比方 GPU 进程等。
渲染进程是浏览器最焦点最重要的进程,而渲染进程下又有多个线程,其中与 JS 关系最大的
就是「JS 引擎线程」。
我们常说的「浏览器中的 JS 是单线程执行的」指的就是浏览器 ( 的一个 Tab 中 ) 有且仅有一
个线程执行你所编写的 JS 代码。
1.浏览器
2.├── 其它进程
3.├── ...
4.├── ...
5.├── 渲染进程 1 // 一个 tab 一个进程
6.│ ├── JS 引擎线程
7.│ ├── HTTP 请求线程
8.│ ├── 定时触发线程
9.│ ├── 事件触发线程
10.│ └── GUI 线程
11.├── 渲染进程 2 // 一个 tab 一个进程
12.│ ├── JS 引擎线程
13.│ ├── HTTP 请求线程
14.│ ├── 定时触发线程
15.│ ├── 事件触发线程
16.│ └── GUI 线程
17.├── ...
18.└── 渲染进程 N // 一个 tab 一个进程
19.├── JS 引擎线程
20.├── HTTP 请求线程
21.├── 定时触发线程
22.├── 事件触发线程
23.└── GUI 线程 JS 引擎线程也被称作『主线程』。
2. 事件循环和执行栈

JS 引擎线程在剖析一段 JS 代码时,会将代码放在某个地方,这个地方就被称作『执行栈』,
然后依次执行内里的函数 ( 代码 ) 。
JS 引擎线程 ( 即,主线程 ) 会执行 JS 代码,但并非全部的 JS 代码都是由 JS 引擎线程 ( 即,主线程 ) 执行!
当在依次执行这些代码过程中,如果遇到了异步代码 ( 比方,发起 ajax 请求、设置定时任务等
) 时,JS 引擎线程就会将这些代码交给其它线程执行 ( 而不是本身亲身、立即执行它们 ) 。当 JS引擎线程把异步任务代码交给别人执行之后,执行栈中的执行流程就继续向下执行 ( 而不是在
异步任务代码处干等、白耗时间 ) 。
当执行栈中的同步代码执行完之后 ( 异步代码都交出去了 ) ,JS 引擎线程会从「一个队列」中
去取出已完成的异步任务的回调,将这个 ( 这些 ) 回调函数放到执行栈中继续执行。如果回调
中又有异步代码,这些异步代码还是会被 JS 引擎线程「交出去」。
比方前面例子中:
「发起 ajax 请求」代码会被交给「HTTP 请求线程」执行;
「设置定时任务」代码会被交给「定时触发线程」执行。
==这些异步任务代码,都不是由 JS 引擎线程执行的!==
被交给「其它线程」执行的异步任务在被「其它线程」执行完成后,它们 ( 异步任务 ) 的回调
函数会被放入『任务队列 ( 即,上面所说的「一个队列」 ) 』,而 JS 引擎线程在执行完同步代码之后,会从「任务队列」中取出这些已完成的异步任务的回调继续执行。
上述工作,JS 引擎线程是『周期性、循环』执行的!这个周期性的循环也被称作事件循环,
每一次循环其实就是一个时间周期,也被称之为一次 tick 。
3. 宏任务和微任务

上面所说的「任务队列」不止一个。根据任务种类的差异可以分为「微任务队列」和「宏任务
队列」。
在事件循环的过程中,执行栈在同步代码执行完成之后,优先会去查抄「微任务栈」中是否有
任务 ( 代码 ) 必要执行,如果没有,再去「宏任务队列」中查看有没有必要执行的任务。如此
往复。口诀:同微宏。
微任务会比宏任务先执行,而且微任务只有一个队列。宏任务队列大概会有多个。
前面举例的 ajax 请求任务和定时器任务都是宏任务,别的,鼠标点击、键盘按下等事件也属于宏任务。
常见的微任务有 promise.then()、promise.catch()、new MutationObserver()、process.nextTick()等。
补充:从某种意义上讲,微任务不能算作严酷意义上的异步任务。由于在底层实现上,V8 引
擎并不会将属于微任务的异步任务交给其它线程执行,仍然是由 JS 引擎线程执行:将它的回
调直接在执行完同步代码之后立即直接执行。逻辑感官上,微任务好似改变了执行次序的同步
代码。
宏任务特性:有明确的异步任务必要执行和回调,必要其它线程支持;
微任务特性:没有明确地任务必要执行,只有回调,不必要其它线程支持。
1.console.log('同步代码1');
2.setTimeout(() => {
3.      console.log('setTimeout');
4.}, 0); // 这里的 0 ,实际上是默认 4 毫秒,它不是真正的 0
5.new Promise((resolve) => {
6.      console.log('同步代码2');
7.      resolve();
8.}).then(() => {
9.      console.log('promise.then');
10.});
11.console.log('同步代码3');

[*]setTimeout 和 promise.then 都是异步执行的,将在所以的同步代码块之后执行;
[*]promise.then 是微任务,而 setTimeout 是宏任务,所以,promise.then 先执行;
[*]new promise 是同步执行的
4. 案例

案例一:在主线程上添加宏任务

1.// 执行结果:1 3 2
2.
3.console.log(1);
4.
5.      setTimeout(function () {
6.      console.log(2);
7.}, 0);
8.
9.console.log(3); 上述代码 3 条语句:


[*]一个打印
[*]一个定时器
[*]一个打印
先执行第一个打印,输出 1 ,然后由于 setTimeout 是异步操作,因此它将被「其它线程」执
行,而它的回调  console.log(2) 会被放入宏队列。接着执行第二个打印,输出 3 ,最后主
线程再取出宏队列中的代码,执行  console.log(2) ,输出 2 。

所以终极的执行结果是 1 3 2 。
案例二:在主线程上添加微任务

1.// 最终执行结果:1 2 4 3
2.
3.console.log(1);
4.new Promise(function(resolve,reject) {
5.      console.log('2');
6.      resolve();
7.}).then(function() {
8.      console.log(3);
9.})
10.console.log(4); 上述代码 4 条语句:


[*]一个打印
[*]一个 new promise
[*]一个 promise.then
[*]一个打印
先执行第一个打印,输出 1 ,然后执行 new Promise ,由于 new Promise 是同步代码,它的参数函数代码会立即执行,所以会执行  console.log(2) 输出 2 。而 promise.then 作为Promise 的回调,会被放入微队列。紧接着执行  console.log(4) 输出 4 ,最后主线程再取出微队列中的  console.log(3) 执行,输出 3 。

所以终极的执行结果是 1 2 4 3。
案例三:宏任务中创建微任务

1.// 最终结果:1 5 11 6 2 3 4 7 8 10 9
2.console.log('1');
3.
4.setTimeout(function () {
5.      console.log('2');
6.      new Promise(function (resolve) {
7.      console.log('3');
8.      resolve();
9.}).then(function () {
10.      console.log('4')
11.})
12.}, 0);
13.
14.new Promise(function (resolve) {
15.      console.log('5');
16.      resolve();
17.}).then(function () {
18.      console.log('6')
19.});
20.
21.setTimeout(function () {
22.      console.log('7');
23.      new Promise(function (resolve) {
24.      console.log('8');
25.      resolve();
26.}).then(function () {
27.      console.log('9')
28.})
29.console.log('10')
30.}, 0);
31.
32.console.log('11');

[*]起首会执行第一个打印,输出 1 ;
[*]然后第 1 个 setTimeout 定时任务会被「其它线程」执行,其回调(  new Promise() {}
)会被放入宏队列,而主线程继续向下;


[*]接着执行第 1 个 new Promise 其代码会立即执行,因此会输出 5 。而它的 .then() 方法
的回调(  console.log(6) )将会被放入微队列;


[*]主线程继续向下,遇到第 2 个 setTimeout 定时任务,它又会被「其它线程」执行,其回
调(  new Promise() {} )会被放入宏队列,而主线程继续向下;


[*]主线程执行  console.log(11) 输出 11 。截止目前为止,输出的是 1 5 11 。然后,主线
程的代码全部执行完,主线程开始从微队列和宏队列中取代码执行。


[*]现在微队列中有且仅有的代码是输出 5 的 new Promise 的回调: console.log(6) 。它
被主线程取出,执行。


[*]微队列当前被清空了,主线程从宏队列中取代码执行。起首取出的是第 1 个定时任务的回
调,先输出 2 ,然后执行 new Promise 输出 3 ,然后 promise.then 的回调
console.log(4) 被放入微队列。


[*]当前主线程代码又执行竣事,然后微队列中有了一条语句,被取出执行,输出 4 。
[*]主线程代码队列和微队列全空,主线从又从宏队列中取代码,执行第 2 个的回调,打印 7
和 8 ,将 promise.then 的回调  console.log(9) 扔进微队列后,继续打印 10 。


[*]最后,主线程从微队列中取出  console.log(9) 执行,输出 9 。
案例四:微任务中创建宏任务

1.// 1 5 2 3 4
2.new Promise((resolve) => {
3.      console.log("1")
4.      resolve()
5.}).then(() => {
6.      console.log("2")
7.      setTimeout(() => {
8.          console.log("3")
9.      }, 0)
10.})
11.
12.setTimeout(() => {
13.      console.log("4")
14.},1000);
15.
16.console.log("5")

[*]起首  new Promise 执行,执行  console.log(1) 输出 1 ,而后 promise.then 被仍进微
队列。


[*]紧接着下面的定时任务代码执行,其回调,会在 4 秒后被「其它线程」扔进宏队列。主线
程继续向下。


[*]主线程执行  console.log(5) 输出 5 ,至此,主线程代码队列中全部执行完,主线程开始
从为队列中拿代码执行。


[*]主线程取出 promise.then 的回调输出 2 ,然后遇到定时器,由于上面定时器隔断时间更
短,因此上面定时器的回调  console.log(3) 会比下面的定时器的回调  console.log(5)
先辈入宏队列,因此,主线程先取到、执行的是输出 3 ,后执行的是输出 4 。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 浏览器中的事件循环