js的事件循环机制

1、js 的定义和特性?

众所周知,js 是一门单线程的非阻塞的脚本语言。javascript

单线程:只有一个调用栈,同一时刻只能干一件事,代码是一段一段执行的。java

调用栈:是一个数据结构,记录咱们程序运行到哪个阶段了,若是调用了函数就进栈,若是函数返回结果,就出栈(进栈出栈)。node

非阻塞:代码须要进行一项异步任务的时候,主线程会挂起这个任务,而后在异步任务返回结果的时候,再根据一段的规则去执行相应的回调。web

为何是单线程的?
这是由于 js 创立之初的目的就在于与浏览器交互,而浏览器要大量操做 dom,试想一下,若是同时对某个 dom 节点进行修改和删除的操做,那会发生什么呢?因此决定了 js 只能是单线程的。ajax

为何非阻塞呢?
咱们在页面中一般会发大量的请求,获取后端的数据去渲染页面。由于浏览器是单线程的,试想一下,当咱们发出异步请求的时候,阻塞了,后面的代码都不执行了,那页面可能出现长时间白屏,极度影响用户体验。后端

2、浏览器环境下?

这里,咱们只谈论 Google 的 js 引擎---V8 引擎(nodeJS 也是 v8 引擎)。api

1.浏览器环境下 js 是怎样工做的?

1.1js 的引擎简图

主要是由两部分组成:promise

  • 1.emory Heap(内存堆) —  内存分配地址的地方
  • 2.Call Stack(调用堆栈) — 代码执行的地方
1.2.js 运行过程简图

  • 1.js 引擎(js 代码执行在调用栈)
  • 2.webapis(浏览器提供给咱们的,不是 js 引擎提供的,例如:Dom,ajax,setTimeout)
  • 3.回调队列(callback queue 包括宏任务,微任务)
  • 4.事件循环(event loop)
1.3.js 运行过程:
  • 1.当 js 运行时,碰到同步任务,就在stack里执行
  • 2.一旦碰到异步任务,主线程会挂起这个任务,把异步回调结果放在callback queue里。
  • 3.等待当前stack中的全部任务都执行完毕,主线程处于闲置状态时,主线程会去查找callback queue是否有任务。若是有,那么主线程会从中取出回调(此处区分宏任务与微任务)放入stack中,而后执行其中的同步代码...,如此反复。
    因为第三步,这样就造成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的缘由

2.浏览器环境下 js 的事件循环机制?

2.1 宏任务和微任务(callback queue回调队列里面2条平行的队列,宏任务队列和微任务队列,宏任务队列里面放宏任务的回调,微任务队列里面放微任务的回调)
  • 宏任务:script(总体代码),setInterval(),setTimeout(),setImmediate(Nodejs), I/O, UI rendering
  • 微任务:process.nextTick(Nodejs),Promises,Object.observe, MutationObserver
2.2 js 事件循环代码
console.log(1);
setTimeout(function a() {
  console.log(2);
}, 0);
new Promise(function (resolve, reject) {
  console.log(5);
  resolve();
}).then(function () {
  console.log(6);
});
new Promise(function (resolve, reject) {
  resolve();
}).then(function () {
  console.log(7);
});
console.log(3);

结果:1,5,3,6,2浏览器

分析:代码从上往下执行,先打印同步任务,1。碰到 setTimeout,把回调函数 a()放到 callback queue 的宏任务里去。而后碰到 Promise,打印 new Promise 的同步任务 5,接着把 then 回调(console.log(6)),放入 callback queue 的微任务里去,而后打印同步任务 3。此时 call stack 为空,去查找 callback queue,微任务比宏任务先,且当前循环会处理当前全部微任务队列中的事件。因此,先打印 6,再打印 7,在打印 2.
总结:先执行同步任务,再执行微任务,最后执行宏任务
2.3.屡次 js 事件循环
let promiseGlobal = new Promise(function (resolve) {
  console.log(1);
  resolve("2");
});
console.log(3);

promiseGlobal.then(function (data) {
  console.log(data);
  let setTimeoutInner = setTimeout(function (_) {
    console.log(4);
  }, 1000);
  let promiseInner = new Promise(function (resolve) {
    console.log(5);
    resolve(6);
  }).then(function (data) {
    console.log(data);
  });
});
let setTimeoutGlobal = setTimeout(function (_) {
  console.log(7);
  let promiseInGlobalTimeout = new Promise(function (resolve) {
    console.log(8);
    resolve(9);
  }).then(function (data) {
    console.log(data);
  });
}, 1000);

执行顺序是 1,3,2,5,6,间隔一秒,7,8,9,4网络

解答以下:

  • 1.打印完 1,3
    本轮执行栈执行完毕
  • 2.打印完 1,3,2,5,6
    微任务队列清空,eventloop 完成,下一次 eventloop 开始
  • 3.打印完 1,3,2,5,6,7
    本轮执行栈执行完毕
  • 4.打印完 1,3,2,5,6,7,8,9
    微任务队列清空,eventloop 完成,下一次 eventloop 开始
  • 5.打印完 1,3,2,5,6,7,8,9,4
    eventloop 完成
⚠️易错点:
 之因此把这道题拿出来说,是由于这道题涉及到屡次事件循环,不少同窗容易搞混的点。

3.总结

  • 当前执行栈执行完毕,会当即处理全部微任务队列中的事件,再去宏任务队列中取出一个事件
  • 在一次事件循环中,微任务永远在宏任务以前执行

2、宏任务、微任务、Dom 渲染的顺序

1.浏览器包含多个进程

  • 1.主进程

    • 协调控制其余子进程(建立、销毁)
  • 2.第三方插件进程

    • 每种类型的插件对应一个进程,仅当使用该插件时才建立
  • 3.GPU 进程

    • 用于 3D 绘制等
  • 4.渲染进程,就是咱们说的浏览器内核(最重要

    • 负责页面渲染,脚本执行,事件处理等
    • 每一个 tab 页一个渲染进程

2.渲染进程包含了多个线程:

  • 1.JS 引擎线程

    • 负责处理解析和执行 javascript 脚本程序
    • 只有一个 JS 引擎线程(单线程)
    • 与 GUI 渲染线程互斥,防止渲染结果不可预期
  • 2.GUI 渲染线程

    • 负责渲染页面,布局和绘制
    • 页面须要重绘和回流时,该线程就会执行
    • 与 js 引擎线程互斥,防止渲染结果不可预期
  • 3.http 请求线程

    • 浏览器有一个单独的线程用于处理 AJAX 请求
  • 4.事件处理线程(鼠标点击、ajax 等)

    • 用来控制事件循环(鼠标点击、setTimeout、ajax 等)
  • 5.定时器触发线程

    • setInterval 与 setTimeout 所在的线程

3. 为何 JS 引擎线程和 GUI 渲染线程是互斥的?

JavaScript 是可操纵 DOM 的,若是在修改这些元素属性同时渲染界面,那么渲染线程先后得到的元素数据就可能不一致了。所以为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JS 引擎为互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时当即被执行。

4. 为何 JS 会阻塞页面加载?

从上面的互斥关系能够推导出,JS 若是执行时间过长就会阻塞页面。譬如,假设 JS 引擎正在进行巨量的计算,此时就算 GUI 有更新,也会被保存到队列中,等待 JS 引擎空闲后执行。而后,因为巨量计算,因此 JS 引擎极可能好久好久后才能空闲,天然会感受到巨卡无比。因此,要尽可能避免 JS 执行时间过长,这样就会形成页面的渲染不连贯,致使页面渲染加载阻塞的感受。

5. JS 引擎线程和 GUI 渲染线程是互斥的,那前后顺序呢?

把下面三段代码放到浏览器的控制台执行:
document.body.style = "background:black";
document.body.style = "background:red";
document.body.style = "background:blue";
document.body.style = "background:grey";

结果:背景直接变成灰色
分析:Call Stack 清空的时候,执行,执行到了 document.body.style = 'background:grey';这时,前面的代码都被覆盖了,此时 dom 渲染,背景色是灰色

document.body.style = "background:blue";
console.log(1);
Promise.resolve().then(function () {
  console.log(2);
  document.body.style = "background:black";
});
console.log(3);

结果:背景直接变成黑色
分析:document.body.style = 'background:blue'是同步代码,document.body.style = 'background:black'是微任务,此时微任务执行完,才会进行 dom 渲染,因此背景色是黑色

document.body.style = "background:blue";
setTimeout(function () {
  document.body.style = "background:black";
}, 0);

结果:背景先一闪而过蓝色,而后变成黑色
分析:document.body.style = 'background:blue';是同步代码,document.body.style = 'background:black'是宏任务,因此 dom 在同步代码执行完,宏任务执行以前会渲染一次。而后宏任务执行完又会渲染一次。2 次渲染,因此才会呈现背景先一闪而过蓝色,而后变成黑色,这种效果。

总结:
1.先把Call Stack清空
2.而后执行当前的微任务
3.接下来DOM渲染
微任务在dom渲染`以前`执行,宏任务在dom渲染`以后`执行。

3、nodeJs 环境下的 js 事件循环机制

⚠️ 注意:如下内容 node 的版本大于等于 11.0.0

1.NodeJs 的架构图


解释:

  • 1.Node Standard Library:Node.js 标准库,这部分是由 Javascript 编写的。使用过程当中直接能调用的 API。例如模块 http、buffer、fs、stream 等
  • 2.Node bindings:这里就是 JavaScript 与 C/C++ 链接的桥梁,前者经过 bindings 调用后者,相互交换数据。
  • 3.最下面一层是支撑 Node.js 运行的关键,由 C/C++ 实现(好比:V8:Google 开源的高性能 JavaScript 引擎,使用 C++ 开发)

2.libuv 引擎

libuv 专一于异步 I/O.是一个基于事件驱动的跨平台抽象层,封装了不一样操做系统的一些底层特性,对外提供统一 API,Node.js的Event Loop 是基于libuv实现的

3.node 环境下 js 是怎样运行的?

  • (1)V8 引擎解析 JavaScript 脚本。
  • (2)解析后的代码,调用 Node API。
  • (3)libuv 库负责 Node API 的执行。它将不一样的任务分配给不一样的线程,造成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
  • (4)V8 引擎再将结果返回给用户

4.js 事件循环的阶段

  • 1.timer:这个阶段执行 timer(setTimeout、setInterval)的回调
  • 2.I/O callbacks 阶段:执行一些系统调用错误,好比网络通讯的错误回调(tcp 错误)
  • 3.idle,prepare 阶段:仅供 node 内部使用,忽略
  • 4.poll 阶段:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎全部状况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的以外),其他状况 node 将在适当的时候在此阻塞。
  • 5.check:setImmediate() 回调函数在这里执行
  • 6.close callbacks:一些关闭的回调函数,如:socket.on('close', ...)

5.process.nextTick 和 microtask 的特别

都是独立于 Event Loop 以外的,它有一个本身的队列,当每一个阶段完成后,若是存在 nextTick 队列,就会清空队列中的全部回调函数,而且优先于其余 microtask 执行。

6.js 事件循环的顺序

  • 宏任务:script(总体代码),setInterval(),setTimeout(),setImmediate(Nodejs), I/O, UI rendering
  • 微任务:process.nextTick(Nodejs),Promises,Object.observe, MutationObserver
setTimeout(funciton(){console.log(1)});
setImmediate(function(){console.log(2)});
process.nextTick(function(){console.log(3)});
Promise.resolve().then(function(){console.log(4)});
(function() {console.log(5)})();

打印结果:5,3,4,1,2

总结:先执行同步任务,接下来执行 process.nextTick,再接下来 Promise 的微任务,最后是 js 事件循环的 6 个阶段,从上到下顺序执行。

⚠️ 注意:每一个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

4、浏览器和nodeJs 环境下的 js 事件循环机制对比

1. 浏览器下打印:time1,promise1,time2,promise2

由于执行完2个定时器,回调都进入宏任务队列了。而后开始事件循环,由于宏任务是一个个执行的,因此先把第一个定时器的回调放入调用栈中,执行完time1,把微任务放入微任务队列中。
这是调用栈清空,又开始事件循环,这时候有微任务promise1,和第二个宏任务。由于微任务在宏任务以前执行,因此先执行promise1,
这是调用栈又清空,又开始事件循环。执行第二个宏任务,打印,time2,promise2

2.node环境下打印:time1,timer2,promise1,promise2

由于已经在timer阶段了,因此。先执行完time阶段,time1,time2,而后看到微任务,执行微任务。

参考文章

1.从多线程到 Event Loop 全面梳理

相关文章
相关标签/搜索