浏览器中的事件循环

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 引擎线程执行:将它的回

调直接在执行完同步代码之后立刻直接执行。逻辑感官上,微任务好似改变了执行顺序的同步

代码。

宏任务特征:有明确的异步任务需要执行和回调,需要其它线程支持;

微任务特征:没有明确地任务需要执行,只有回调,不需要其它线程支持。

javascript 复制代码
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. 案例

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

javascript 复制代码
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 。

案例二:在主线程上添加微任务

javascript 复制代码
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。

案例三:宏任务中创建微任务

javascript 复制代码
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 。

案例四:微任务中创建宏任务

javascript 复制代码
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 。

相关推荐
貂蝉空大3 分钟前
vue el-table组件实现展开行 默认展开全部
javascript·vue.js·element-plus
猫猫村晨总7 分钟前
涉及到行合并的el-table表格导出功能实现
前端·vue.js·element plus
VillanelleS22 分钟前
Vue2进阶之Vue3高级用法
前端·javascript·vue.js
天农学子26 分钟前
EasyUI弹出框行编辑,通过下拉框实现内容联动
前端·javascript·easyui
格瑞@_@1 小时前
11.Three.js使用indexeddb前端缓存模型优化前端加载效率
前端·javascript·缓存·three.js·indexeddb缓存
木子七1 小时前
Js Dom
前端·javascript
写bug写bug1 小时前
Git 中的撤销工作区、暂存区和已提交的更改
前端·git·后端
重生之我是菜鸡程序员2 小时前
uniapp 使用vue/pwa
javascript·vue.js·uni-app
谢小飞2 小时前
我做了三把椅子原来纹理这样加载切换
前端·three.js
圈圈的熊2 小时前
HTTP 和 HTTPS 的区别
前端·网络协议·http·https