IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 事件循环 js事件循环、node事件循环、浏览器事件循环 -> 正文阅读

[JavaScript知识库]事件循环 js事件循环、node事件循环、浏览器事件循环

1. 事件循环

JS引擎是单线程的,一个时间点下JS引擎只能做一件事情。

在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。
Node环境中,只有JS 线程。

JS的任务分为异步和同步两种

  • 异步:一个任务不是连续完成的,先执行第一段,做好准备之后,再回过头执行第二段(回调)。
  • 同步:连贯完成的。

eg:读取文件、网络请求等任务属于异步任务:花费时间长,但中间的操作不需要JS引擎自己完成,它只需要等别人准备好,把数据给它,它执行回调部分。

如果没有特殊处理,JS 引擎在执行异步任务时,应该是存在等待的,不去做任何其他事情。用一个图来展示这个过程,可以看出,在执行异步任务时有大量的空闲时间被浪费。
在这里插入图片描述

为了不使JS引擎处于忙等的状态,采取了异步任务回调通知模式:
在这里插入图片描述

在等待异步任务准备的同时,JS 引擎去执行其他同步任务,等到异步任务准备好了,再去执行回调。这种模式的优势显而易见,完成相同的任务,花费的时间大大减少,这种方式也被叫做非阻塞式。

实现这个“通知”的,正是事件循环,把异步任务的回调部分交给事件循环,等时机合适交还给 JS 线程执行。事件循环并不是 JavaScript 首创的,它是计算机的一种运行机制。


2. 任务队列

事件循环是通过任务队列的机制进行协调的。

一个事件循环中,可以有一个或者多个任务队列(task source),源自同一个任务源的task必须放到同一个任务队列,从不同源来的则被添加到不同的队列。

事件循环是由一个队列或者多个队列组成的,异步任务的回调遵循先进先出,在 JS 引擎空闲时会一轮一轮地被取出,所以被叫做循环。
在这里插入图片描述

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。
也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。

Event Loop(事件循环)中,每一次循环称为tick,每一次tick的任务如下

  • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)
  • 检查是否存在 Microtasks,如果存在则不停地执行,直至清空 Microtasks Queue
  • 更新 render
  • 主线程重复执行上述步骤。

对于每一次循环tick:

  • JS分为同步任务和异步任务。
  • 同步任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有运行结果,就在任务队列中放置一个事件。
  • 一旦执行栈中所有同步任务执行完成(JS引擎空闲),系统就会读取任务队列,将可运行的任务队列加入执行栈中,开始执行。

根据队列中任务的不同,分为宏任务微任务


3. 宏任务、微任务

事件循环由宏任务和执行宏任务期间产生的所有微任务组成。
完成当下的宏任务后,会立即执行所有在此期间入队的微任务。
在这里插入图片描述

这种设计是为了给紧急任务一个插队的机会,否则新入队的任务永远被放在队尾。
区分了微任务和宏任务后,本轮循环中的微任务实际上就是在插队,这样微任务中所做的状态修改,在下一轮事件循环中也能得到同步。

举个小例子:

console.log('script start');
 
setTimeout(function() {
  console.log('setTimeout');
}, 0);
 
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
 
console.log('script end');

浏览器环境下、node环境下输出结果都为:
在这里插入图片描述

分析:

 1. 执行console.log()输出script start
 2. 遇到setTimeout加入宏队列
 3. 遇到两个Promise.then()加入内部队列
 4. 遇到console.log()输出script end
 5. 微任务队列的任务依次取出放入执行栈执行 输出promise1 promise2
 6. 宏任务队列的任务执行。

这个例子就涉及到了JavaScript事件轮询中的宏任务和微任务。

ES6 规范中,宏任务(microtask) 称为 jobs,微任务(macrotask) 称为task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。

在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

宏任务(macrotask)微任务(microtask)
谁发起的宿主(Node、浏览器)JS引擎
具体事件1. script (可以理解为外层同步代码)
2. setTimeout/setInterval
3. UI rendering/UI事件
4. postMessage,MessageChannel
5. setImmediate,I/O(Node.js)
1. Promise
2. MutaionObserver
3. Object.observe(已废弃;Proxy 对象替代)
4. process.nextTick(Node.js)
谁先运行后运行先运行
会触发新一轮Tick吗?不会

宏任务setTimeout的误区

setTimeout 的回调不一定在指定时间后能执行。

而是在指定时间后(异步任务有结果),将回调函数放入事件循环的队列中。

如果时间到了,JS 引擎还在执行同步任务,这个回调函数需要等待;
等到JS引擎空闲,从任务队列中取出进行处理,如果当前事件循环的队列里还有其他回调,需要等其他回调执行完。

另外,setTimeout 0ms 也不是立刻执行,它有一个默认最小时间,为 4ms。

4. 浏览器的事件循环

浏览器的事件循环是由一个宏任务队列+多个微任务队列组成。

  1. 执行第一个宏任务:全局Script脚本。产生的宏任务和微任务进入各自的队列中。执行完Script后,把当前的微任务队列清空(进行处理)。完成一次事件循环。
  2. 再取出一个宏任务,把在此期间产生的回调入队。再把当前的微任务队列清空,往复如此。

宏任务队列只有一个,而每一个宏任务都有一个自己的微任务队列,每轮循环都是由一个宏任务+多个微任务组成。

// Promise1
Promise.resolve().then(() => {
    console.log('微任务1');
    // setTimeout2
    setTimeout(() => {
        console.log('宏任务2');
    },0)
})

// setTimeout1
setTimeout(() => {
    console.log('宏任务1');
    // Primse2
    Promise.resolve().then(() => {
        console.log('微任务2');
    })
},0)

执行结果:
在这里插入图片描述

过程分析:
没开始第一次事件循环的时候:宏任务队列中[script];

  1. 第一次事件循环:
    取出宏任务script开始执行,
    此时微任务队列:[promise1];
    此时的宏任务队列:[setTimeout1]
    清空当前的微任务队列,一次放入执行栈中,
    此时输出‘微任务1’,setTimeout2进入宏任务队列
    此时的宏任务队列[setTimeout1,setTimeout2]
  2. 第二次事件循环:
    宏队列中取出setTimeout1,开始执行;
    输出‘宏任务1’
    此时微任务队列:[Promise2];
    执行此时的微任务队列中的任务,输出‘微任务2’,微任务队列清空。
  3. 第三次事件循环:
    取出宏任务setTimeout2开始执行
    输出‘宏任务2’

5. Node的事件循环

node事件循环比浏览器复杂,由6个宏任务队列+6个微任务队列组成。

宏任务按照优先级从高到低依次是:

在这里插入图片描述

其执行规律是:在一个宏任务队列全部执行完毕后,去清空一次微任务队列,然后到下一个等级的宏任务队列,以此往复。

一个宏任务队列搭配一个微任务队列。六个等级的宏任务全部执行完成,才是一轮循环。

  1. timers阶段: 用来执行timer(setTimeout()、setInterval())的回调
  2. I/O callbacks阶段: 处理上一轮循环中少数未执行的I/O回调
  3. idle prepare阶段:仅node内部使用,我们用不到
  4. poll阶段:获取新的I/O时间,适当的条件下node将阻塞在这里
  5. check阶段:执行setImmediate()的回调
  6. close callbacks:执行socket的close时间回调

需要关注的阶段:
Timers阶段

  • 执行定时器 setTimeout/setInterval回调,并且是由poll阶段控制的。
  • 同样,在node中定时器指定的时间也不是准确时间,只能是尽快执行。

Poll阶段

系统做的两件事情:

  1. 回到timer阶段执行回调、
  2. 执行I/O回调

Check:setImmediate 回调函数在这里执行;

除此之外,node 端微任务也有优先级先后:

  1. process.nextTick;
  2. promise.then 等;

eg:

console.log('script开始');
setTimeout(() => {
    console.log('宏任务1');
    Promise.resolve().then(function () {
        console.log('微任务2')
    })
},0);

setTimeout(() => {
    console.log('宏任务2');
    Promise.resolve().then(function() {
        console.log('微任务3')
    })
})

Promise.resolve().then(function () {
    console.log('微任务1');
})

console.log('script结束');

node端运行结果:
script开始
script结束
微任务1
宏任务1
微任务2
宏任务2
微任务3

浏览器端运行结果:
script开始
script结束
微任务1
宏任务1
微任务2
宏任务2
微任务3

主要区别在于:

  • 浏览器是一个宏任务+一个微任务队列
  • node是一个宏任务队列+一个微任务队列

6. node 11.X前后版本区别

node11.x 之前,其事件循环的规则就如上文所述:先取出完一整个宏任务队列中全部任务,然后执行一个微任务队列。

但在 11.x 之后,node 端的事件循环变得和浏览器类似:先执行一个宏任务,然后是一个微任务队列。但依然保留了宏任务队列和微任务队列的优先级。

eg:

console.log('Script开始')
setTimeout(() => {
  console.log('宏任务1(setTimeout)')
  Promise.resolve().then(() => {
    console.log('微任务promise2')
  })
}, 0)
setImmediate(() => {
  console.log('宏任务2')
})
setTimeout(() => {
  console.log('宏任务3(setTimeout)')
}, 0)
console.log('Script结束')
Promise.resolve().then(() => {
  console.log('微任务promise1')
})
process.nextTick(() => {
  console.log('微任务nextTick')
})

node11.x之前运行:
Script开始
Script结束
微任务nextTick
微任务promise1
宏任务1(setTimeout)
宏任务3(setTimeout)
微任务promise2
宏任务2(setImmediate)

node11.x之后运行:
Script开始
Script结束
微任务nextTick
微任务promise1
宏任务1(setTimeout)
微任务promise2
宏任务3(setTimeout)
宏任务2(setImmediate)

可以发现,在不同的 node 环境下:

  • 微任务队列中 process.nextTick 都有更高优先级,即使它后进入微任务队列

  • 在 node11.x 之前,微任务队列要等当前优先级的所有宏任务先执行完

  • 在 node11.x 之后,微任务队列只用等当前这一个宏任务先执行完。


结语:

  • 事件循环中的任务被分为宏任务和微任务,是为了给高优先级任务一个插队的机会:微任务比宏任务有更高优先级。
  • node 端的事件循环比浏览器更复杂,它的宏任务分为六个优先级,微任务分为两个优先级。
  • node 端的执行规律是一个宏任务队列搭配一个微任务队列,而浏览器是一个单独的宏任务搭配一个微任务队列。但是在 node11
    之后,node 和浏览器的规律趋同。
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 20:40:43  更:2022-03-21 20:43:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:24:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码