关于javascript:js的事件循环机制

9次阅读

共计 6134 个字符,预计需要花费 16 分钟才能阅读完成。

一、js 的定义和个性?

家喻户晓,js 是一门单线程的非阻塞的脚本语言。

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

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

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

为什么是单线程的?
这是因为 js 创建之初的目标就在于与浏览器交互,而浏览器要大量操作 dom, 试想一下,如果同时对某个 dom 节点进行批改和删除的操作,那会产生什么呢?所以决定了 js 只能是单线程的。

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

二、浏览器环境下?

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

1. 浏览器环境下 js 是怎么工作的?

1.1js 的引擎简图

次要是由两局部组成:

  • 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. 总结

  • 以后执行栈执行结束,会立刻解决 所有 微工作队列中的事件,再去宏工作队列中取出 一个 事件
  • 在一次事件循环中,微工作永远在宏工作 之前 执行

二、宏工作、微工作、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 渲染 ` 之后 ` 执行。

三、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 个阶段,从上到下程序执行。

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

四、浏览器和 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 全面梳理

正文完
 0