中探:事件循环相关内容(因为不仅仅是初步认识,但也不至于是深入探讨,所以命名为“中探”)

news2024/12/27 11:56:44



下面内容写于 2022 年,文本描述过多,可能不适合有经验的人看。新的文章在 个人网站 中。

对了,说到事件循环,怎么可以离开这个最知名的视频呢!视频是英文的,但即使你听不懂,单纯看他的操作,我相信你也能够对事件循环有了清晰的认识!




🍕 涉及到的概念

  • 事件循环 (Event Loop)
  • 宏任务 (macrotask)
  • 宏任务队列 (macrotask queue)。在 WHATWG specification 中被简单地称为 task queue1
  • 微任务 (microtask)
  • 微任务队列 (microtask queue)
  • 执行栈, 或调用栈 (Call Stack)
  • 任务队列 (Task Queue, Event Queue), 我这里将 宏任务队列微任务队列 统称为 任务队列
  • 异步编程 (asynchronous programming)
  • 单线程 (single-thread)

🍕 通过一段代码, 来简单认识 Event Loop

我这里先做定义一个 异步任务源, 任务源可以分配任务, 异步任务源 就是专门分配 异步任务 的。这个想法来自 task-source | WHATWG。我这里不对定义做过多说明,直接看我的例子, 你应该就明白我想表达什么了:

比如 setTimeout(callback) 中的 setTimeout 是一个 异步任务源, 它的参数 callback 就是一个 异步任务, setTimeout 本身的执行是同步的, 只不过在它执行的过程中, 它创建一个 异步任务, 这个 异步任务 不会立马执行。

function test () {
  console.log('1') // f1
  setTimeout(fn, delay/* 假设是500ms */) // f2
  console.log('2') // f3
}
;(function main () {
  test()
})()

我专门画了一张图, 来简单描述一下过程

在这里插入图片描述

Event Loop

  1. 首先, main() 会进入 执行栈, 然后 执行栈 会自上而下执行该函数中的各条语句
  2. 在执行语句的过程中, 如果遇到形如同步函数, 比如 test(), 那么它会先等待其执行完毕, 也就是会将 test() 压入栈, 然后继续执行 test() 中的各条语句
  3. 在执行 f1 时, 执行栈 又遇到了同步函数 console.log, 于是它继续先等待其执行完毕, 也就是会先等屏幕输出 1, 然后才继续开始下一条语句
  4. 接着, 执行栈 遇到了 异步任务源, 也就是 setTimeout, 于是他会将 setTimeout 分配的 异步任务(包含 fn 和 delay) 送到某个区域, 这个区域我们先称其为 异步模块 (在浏览器环境中叫 APIs), 然后 执行栈 就会继续执行下一条语句了。注意此时 fn 并没有执行。

APIs 的执行由浏览器单独负责, 他和 JS 的单线程没有关系。记住: JS 是单线程的, 但浏览器是多线程的。 2

  1. 执行栈 接着执行, 然后会输出 2, 此时主函数执行完, 执行栈 中没有其他的任务需要执行了。
  2. 执行栈 为空时, 会让 Event Loop 从一个区域中取出新的任务执行, 这个区域我们称之为 任务队列, 任务队列 中的任务都是等待程序处理的任务, 这些任务的来源就是我们刚刚提到的 异步模块。因为此时时间还没有过去太久(不足500ms), 所以 任务队列 为空, 执行栈 也为空
  3. 异步模块 每过一段时间就会查看一下它维护的那些 异步任务, 经过大约 500ms(实际数值肯定大于500ms) 后, 异步模块 发现有一个 异步任务(fn) 可以执行了, 于是将这个 fn 发送给 任务队列, 此时 任务队列 不为空
  4. 执行栈 为空, 并且 任务队列 有任务在等待执行, 于是 Event Loop 从中取出任务(fn), 并发送到 执行栈 中执行, 于是 执行栈 继续执行, 并输出了 3

从上面的步骤可以看到, 一个重要的时间点就是 执行栈 为空, 并且这个时间点后, 相关操作都是由 Event Loop 处理的, Event Loop 负责调控整个流程。 当然 异步模块 中的内容是另外的一部分, 正因如此, 当 执行栈 中执行任务时, 异步模块 可以对异步任务进行计时。

总的来说, 执行栈 执行任务, 执行完后, 就会让 Event Loop任务队列 中取出新的任务送到 执行栈 中执行。注意, 当 执行栈 不为空时, 任务队列 中的任务是无法进入 执行栈 中执行的。

现在, 让我们了解两个新名词, 宏任务微任务, 前面的 异步任务 并不是常见的名词, 而是我自己在此处定义的名词, 异步任务 其实就是 宏任务微任务 的总称。

🍕 宏任务和微任务

宏任务微任务 是两类不同的 异步 任务。3

宏任务队列 的数据结构 不是队列, 而是 集合4
记住这个概念很重要, 因为队列是有序的, 而集合是无序的, 所以在 宏任务队列 中, 先到达的任务 不一定 会先执行。

微任务队列 的数据结构是 队列, 所以 微任务队列 中任务的执行一定是有序的。微任务队列 还有这么一个特点, 当 微任务队列 中的 微任务 开始执行时, 它可以继续添加新的 微任务微任务队列 中, 并且 微任务队列 一旦开始执行, 就会执行到 微任务队列 为空。换句话说, 如果不断的有新的 微任务 加入到 微任务队列 中来, 那么 宏任务 将不断的被阻塞, 无法执行。这种情况导致的最常见的后果就是页面无法响应你的鼠标或者滚轮, 因为与用户的交互是属于 宏任务为了处理无限递归的 微任务, 听说以前的 Nodejs 中, 会提供一个机制来限制最大的递归数量, 但我没有在官方文档中找到具体的内容。from1, from2

微任务队列 中的任务会一次性执行完, 带来的好处是它确保了每一个 微任务 之间的应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)。5

如果要给 微任务宏任务 定一个优先级, 那么你可以认为 微任务 的优先级更高。但我认为, 与其记住谁的优先级更高, 不如记住这么一句话: 每一个宏任务执行之前, 必须确保微任务队列为空6

下面给出已知的 宏任务微任务

  • 宏任务
    • setTimeout
    • setInterval
    • setImmediate (Node 独有)
    • requestAnimationFrame (浏览器独有)
    • I/O
    • UI rendering (浏览器独有)
  • 微任务
    • process.nextTick (Node 独有)
    • Promises (准确的说是 Promise.then() 中 then 的回调函数, 而不是 new promise(callback) 携带的回调函数)
    • Object.observe
    • MutationObserver
    • queueMicrotask

🍕 通过一段代码来理解宏任务和微任务

setTimeout(() => { // l-1
  console.log("宏任务: 计时任务1") // l-3
  Promise.resolve().then(() => { // l-4
    console.log("微任务1") // l-5
  })
}, 500);

setTimeout(() => { // l-2
  console.log("宏任务: 计时任务2") // l-6
  Promise.resolve().then(() => { // l-7
    console.log("微任务2") // l-8
  })
}, 500);

先讨论真正有用的, 也就是Node11之后版本和浏览器的版本, 下面以浏览器内核进行解释:

  • l-数字 代表某行代码
  • APIs 是浏览器中的一个机制, 详细的结果不清楚, 只知道一些异步API的处理, 都是由它进行处理的, 当异步函数执行完毕时, 也是由它负责发送给 任务队列7
  • 宏任务队列, 由宏任务组成的队列, 宏任务队列分为 计时器队列 (Expired Timer Callbacks, 即到期的setTimeout/setInterval)、IO事件队列(I/O Events)、即时队列 (Immediate Queue, 即 setImmediate)、关闭事件处理程序队列 (close Handlers)。8 9
  • 微任务队列, 由微任务组成的队列
  1. 首先, 浏览器自上而下的执行(执行的过程中 执行栈 中进行), 先执行 l-1, 发现是 setTimeout, 于是将它的参数(callback,delay)发送给 APIs
  2. 然后继续识别 l-2, 发现又是 setTimeout, 于是继续将它的参数发送给 APIs
  3. APIs 接收到 setTimeout 的内容后, 会进行计时, 当经过 delay(也就是500ms) 后, 会将 callback(也就是l-1的回调函数) 发送给 任务队列 中的 宏任务队列。这样的事情 APIs 干了两次(因为有两个 setTimeout)。
  4. (继2) 当 执行栈 为空时, Event Loop 会将 任务队列 中的任务发送到 执行栈 中执行。不过此时 任务队列 为空, 故什么都不执行
  5. 经过 delay 时间后, 两个计时器的回调函数将会被 APIs 发送给 宏任务队列
  6. 此时 执行栈 为空, 并且 微任务队列 为空, 宏任务队列 非空, 故可以将 宏任务队列 中的第一个 宏任务 送到 执行栈 中执行
  7. 执行栈 执行 l-1 的回调函数, 先执行 l-3, 此时直接输出 "宏任务: 计时任务1"
  8. 继续执行 l-4, 发现是 Promise, 于是将 then 的回调函数送到 APIs (反正是类似 APIs 的异步处理模块, 但该模块不属于 JS 的单线程范畴), 然后执行了 resolve 了, 于是 then 的回调函数被发送到了 微任务队列
  9. 执行栈 执行完 l-3l-4 后, 又为空了, 于是 Event Loop 继续查看 任务队列
  10. 此时的 任务队列 中, 微任务队列 有了新的 微任务, 故先执行 微任务, 也就是将 l-5 送入 执行栈 中执行, 此时输出 "微任务1"
  11. 执行完 l-5 后, 执行栈 为空, 微任务队列 为空, 于是 Event Loop 再从 宏任务队列 中取出一个 宏任务 送往 执行栈
  12. 后面的就和前面的重复(循环)了
  13. 先执行 l-6, 输出 "宏任务: 计时任务2"
  14. 在执行 l-7, 执行完后会 微任务队列 又增加了一个 微任务
  15. 执行栈 又为空, 继续查看 任务队列, 取出 微任务 送往 执行栈
  16. 执行 l-8, 输出 "微任务2"
  17. 结束, 任务队列 又为空, 不作任何操作, 等待异步模块继续发送某些 宏任务微任务任务队列 中, 比如用户突然点击了某个绑定了回调事件的按钮, 或者某个网络请求请求结束, 或者是之前设置的某些定时任务到了触发的时间了等等…

看完上面的过程, 我们其实可以发现, 打开网页时, 除了网页文档中的 script 脚本是直接送入 执行栈, 其他的 任务, 其实都是从 任务队列 中取出的了。或者更加简单一点, 我们可以直接认为, 文档中最开始的 script 其实就是在 任务队列 中的。可能是 宏任务, 也可能是 微任务。反正记住一点, 最先开始执行的肯定是 script 脚本中的内容, 其他的内容, 就都是从 任务队列 中取出的了, 而 任务队列 中的内容, 是由异步模块(比如 APIs, 其实我也就只知道一个 APIs 了)发送给我们的。

对于 宏任务微任务, 不需要在意谁先执行谁后执行, 只需要记住一点就可以了: 当一个 宏任务 想要执行时, 必须确保 微任务队列 为空。记住这一点后, 其他的都能够直接推理出来了, 比如 微任务队列 不为空时, 永远轮不到 宏任务 执行, 换句话说, 我们要小心使用 微任务队列, 不然会出现死循环的情况, 这也是为什么官网不建议我们用太多 queueMicrotask() 函数。

🍕 NodeJS11 之前的 Event Loop (不重要, 可忽略)

下面来谈一点 “过时” 的东西, 前面的分析, 在现在这个时间点(22.12.15)都是对的。而在之前, 也就是 NodeJS11版本之前(不包括11), node 和 浏览器的 Event Loop 机制是不一样的, 最大的区别就在于 宏任务微任务。前面已经说了,每一个 宏任务 执行时, 都要确保 微任务队列 为空, 这是新版本的标准。在此之前的版本有一点点不同,之前的版本所要求的的是 同一类宏任务队列 执行之前, 要确保 微任务队列为空。这个差异导致的结果就是,当存在两个 setTimeout 时, 会先执行完这两个宏任务, 然后再去执行微任务, 所以前面的代码, 用 node11 之前的版本运行时, 会是不一样的结果


node 10 vs 16

有关 nodejs 的 Event Loop 具体的流程图, 可以看下面这张图 10


11版本之前的流程图

在这个过程中, 还发现了一个 让人困惑 有意思的现象, 那就是当我们将两个 setTimeoutdelay 设置为 0 秒时, 输出的情况是不确定的, 有时候会出现 微任务1宏任务2 之前输出, 如图所示


nodejs10 different output

下面我想试着解释这么一种现象。

首先, 通过输出可以发现, 大多数情况下, 还是先输出两个宏任务, 然后才输出微任务, 这个很好理解, 当 宏任务队列 存在两个 setTimeout 时, 肯定会先执行完两个 setTimeout 后再去查看 微任务队列 (注意这是 NodeJS11 版本之前, 新版本不是这样的)。

那么什么情况下, 会出现先输出 宏任务1微任务1 呢?, 我认为关键就在于程序具体执行的细节中。上一段话中, 我们说了 宏任务队列 存在两个 setTimeout 时, 肯定会先执行完两个 setTimeout, 但实际运行时, 宏任务队列 中一定会存在两个 setTimeout 吗? 或者应该这么问, 当第一个 setTimeout 运行完后, 另外一个 setTimeout 真的存在 宏任务队列 中吗? 答案应该是不一定的, 让我逐帧来分析一下:

在开始分析之前, 容许我再重复一下上面的代码:

setTimeout(() => { // l-1
  console.log("宏任务: 计时任务1") // l-3
  Promise.resolve().then(() => { // l-4
    console.log("微任务1") // l-5
  })
}, 0)

setTimeout(() => { // l-2
  console.log("宏任务: 计时任务2") // l-6
  Promise.resolve().then(() => { // l-7
    console.log("微任务2") // l-8
  })
}, 0)
  1. 首先, l-1l-2 都会在 执行栈 中等待执行
  2. 执行栈 先执行 l-1, 此时会将 l-3,4,5 送到 异步模块 中。为方便描述, 记 l-3,4,5s1
  3. 执行栈 继续执行 l-2, 此时会将 l-6,7,8 送到 异步模块 中。 记 l-6,7,8s2

因为 异步模块 不属于 JS 单线程的范畴, 所以 异步模块 的内容和 执行栈 中的内容是可以并发进行的, 这就导致了一种分歧: 当 执行栈 为空时, 异步模块 可能还未将 s1 发送给 宏任务队列, 也可能已经将 s1 发送给 宏任务队列
理由: 我们设置的延迟时间是 0, 理论上 s1 被送往 异步模块 时, 异步模块 应该马上将其发送给 任务队列, 但实际上, 异步模块 应该会每隔一段时间, 才检查 s1 的延迟时间是否已经到期, 才决定是否将 s1 送往 宏任务队列
下面我们考虑的是情况是, 当 执行栈 为空时, 只有 s1 已经被送往 宏任务队列

  1. 执行栈s2 送往 异步模块 后, 执行栈 为空, 此时 宏任务队列 只有 s1, 微任务队列 为空, 于是 Event Loops1 送往 执行栈 执行
  2. 关键来了!, 存在这么一个时间节点, 执行栈 在执行 s1, 异步模块 在等待 s2 的时间到期, 任务队列 为空。 执行栈 先执行完 s1 时, l-4 (Promise callbacks) 会被送往了 异步模块, 并且 异步模块 还未将 s2 送往 宏任务队列。 也就是说, 此时的 异步模块 同时存在 s2l-4 (Promise callbacks), 并且 异步模块 会先将微任务 l-4 (Promise callbacks) 送往 微任务队列, 而 s2 还停留在 异步模块 中。

虽然我不清楚 异步模块 具体实现的源代码, 甚至都不敢保证 node 中存在 异步模块 这个机制, 但因为 微任务 是优于 宏任务 的, 所以, 当同时存在 0 秒延迟的 setTimeout 和微任务(Promise callbacks)时, 即使 setTimeout 是率先到达 异步模块 的, 我也认为微任务是有机会先于 setTimeout 被发送到 任务队列 的。

  1. 异步模块 先将 l-4 发送到 微任务队列, 此时, 执行栈 为空, s2 还未送往 宏任务队列, 微任务队列 中存在 l-4, 于是 Event Loop 就先将 l-4 送往 执行栈 中执行了, 从而导致了 微任务1 先于 宏任务2 输出。后面的分析就没有什么需要做笔记的了。

总的来说, 我的解释就是: 存在这么一个时间节点, 异步模块 中同时存在 宏任务2微任务1。并且, 虽然 宏任务2 先于 微任务1 进入 异步模块, 但 异步模块 还是可能先将 微任务1 发送到 任务队列, 从而导致了 微任务1 先于 宏任务2 执行。

其实, 我不确定上面的解释是否是正确的, 因为我不清楚 nodejs 的源代码是如何编写的, 更不确定 nodejs 是否真的存在一个 异步模块, 但因为浏览器存在一个 APIs, 所以我感觉 nodejs 可能也有一个 异步模块

下面我再用别人给出的 nodejs11版本之前的 Event Loop 图, 来解释一下:


关键的时间点1, setTimeout1 开始执行, 但 setTimeout2 还没有进入宏任务队列中


关键时间点2, 只要 微任务1 能够在 s2 还未执行时进入到队列中, 那么它就有很大概率先于 s2 执行

好了, 这一部分, 仅仅只是感觉有意思的, 所以想着纪录一下, 初次学习(说的就是我)不要 只想不做, 不然容易误入歧途, 最好休息一下, 放空放空大脑, 或者去看看大佬的文章, 再回头来思考一下。

🍕 总结

  • Event Loop 是一种机制, 它指示了异步任务任务之间的运行规则。
  • JS 的单线程, 体现在 执行栈 只有一个, 并且只有 执行栈 为空时, 才有机会将新的任务送入 执行栈 中执行。
  • 每一个宏任务执行之前, 必须确保 微任务队列 为空。两个 setTimeout 的回调函数, 属于两个宏任务。

🍕 参考资料

  • JS 的异步机制一探 - ByteEE
  • NodeJS Event Loop: JavaScript Event Loop vs Node JS Event Loop - Deepal Jayasekara(很优秀的系列文章, 这里有翻译版, 不过最新的一篇没有翻译 —— 有关Nodejs和浏览器的对比)
  • Difference between microtask and macrotask within an event loop context - stack overflow
  • Event Loop - WHATWG(想要更深入时必读)
  • what is an event loop in javascript, (不长, 可简单的认识什么是 Event Loop)
  • JavaScript Event Loop, (不是很长, 介绍的是 Event Loop, 图表更丰富一些)
  • Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript
  • event-loop
  • 带你了解事件循环机制(Event Loop)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1508079.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用gin框架,编写一个接收数据的api接口

功能:这里主要编写一个接口,将其json 数据存入对应的redis队列中,并统计每天的每小时请求数量 环境: go version go1.22.0 linux/amd64 平台 linux X64 步骤一 新建目录 命令如下: mkdir FormData 步骤二 新增…

【Linux】Linux上的一些软件安装与环境配置(Centos7配置JDK、Hadoop)

文章目录 安装JDK配置环境变量1. 卸载已安装的JDK查询已安装的 jdk 列表删除已经安装的 jdk 2. 上传安装包3. 创建 /usr/local/java 文件夹4. 将 jdk 压缩包解压到 /usr/local/java 目录下5. 配置 jdk 的环境变量6. 让配置文件生效7. 校验8.拍个快照吧,免得后面哪里…

2024 年系统规划与管理师(全套资料)

2024年11月系统规划与管理师全套视频、历年真题及解析、章节分类真题及解析、论文写作及范文、教材、模拟题、答题卡等资料 1、2023年5月、2022年5月、2021年5月、2020年5月四套基础精讲视频,案例分析及论文答题套路视频讲解。 2、系统规划与管理师2017-2023年真题…

【go语言开发】redis简单使用

本文主要介绍redis安装和使用。首先安装redis依赖库,这里是v8版本;然后连接redis,完成基本配置;最后测试封装的工具类 文章目录 安装redis依赖库连接redis和配置工具类封装代码测试 欢迎大家访问个人博客网址:https://…

labview的常用小技巧

1.切换:labview中控件函数与函数选板的使用非常频繁,而使用菜单来调用他们非常不方便。最简单的调用方法是:右击前面板,弹出控件选板;右击程序框图,弹出函数选板。然后按住CtrlE组合键,即可快速…

工业数据采集网关的功能与应用-天拓四方

工业数据采集网关是一种专门用于采集、处理、传输工业现场数据的设备。它能够实时收集来自各种传感器、仪表和设备的数据,并通过网络将这些数据传输到云端或数据中心。同时,数据采集网关还具备数据清洗、转换和压缩等功能,确保数据的质量和传…

MySQL将两条记录根据相同条件合并

知识点:在MySQL中,可以使用GROUP BY子句和聚合函数如CONCAT或CONCAT_WS来将多条记录基于相同条件合并为一条记录 【主要是GROUP_CONCAT这个函数的运用】 例如将员工信息表中相同门店的员工信息合并为一条记录 MySQL语句如下: SELECT dept_…

一文读懂:公网IP地址证书

公网IP证书是一种SSL证书,用于验证和确认特定的公网IP地址是否实际属于申请者。如果验证通过,证书颁发机构将向该IP地址持有人颁发一个以IP地址为主题的SSL证书。使用公网IP证书可以有效提升IP身份的辨识度,减少网站链接被假冒的风险&#xf…

建模杂谈系列237 使用FSM进行状态管理

说明 使用FSM来对状态的变化进行管理,一方面有助于我们将问题定义的更清晰,同时也让程序设计更可靠、可读性(事后)更强。 内容 1 问题描述 假设有一笔投资用于证券交易,随着交易、市场价格变化,投资的状态也随之改变。我们需要…

巫蛊之祸——汉武帝后期的一次重大事件

引 言 “巫蛊之祸”是汉武帝在位后期发生的一次重大政治事件,也是西汉历史上最大的冤案,此案导致皇后卫子夫和太子刘据自杀,数万人头落地,几十万人被牵连。 一、巫蛊之术的由来 《汉书》记载,巫蛊之术起源自胡巫&am…

解决Iterm2升级后遇到“Stashed changes“的问题

<<<<<<< Updated upstream ...... >>>>>>> Stashed changes冲突标记符的代码如题,最近有升级Item2…

ROS的消息发布者与订阅者示例

前言 Topic话题,是节点之间信息交换的方法,在向话题发生送消息的节点叫做发布者,接收消息的节点叫做订阅者。 一个ROS程序中话题可以有很多个,一个话题中也可以有多个发布者和订阅者。一个订阅者可以订阅多个话题。同样 &#xff…

Linux 地址空间

目录 一、程序地址空间 1、虚拟地址 Makefile新写法 2、进程地址空间分布 3、栈&堆 4、static修饰局部变量 5、字符串常量不可修改 6、虚拟地址与物理地址的联系 二、CPU读取程序全过程 1、形成可执行程序 2、生成虚拟地址 3、程序的启动 4、创建进程 5、地…

MyBatis-Plus生成sql语句时怎么知道表名和表的字段名,表的主键名的

MyBatis-Plus通过反射获取实体类的信息。 实体类的类名驼峰转下划线为表名 实体类的属性名驼峰转下划线为字段名 表的主键名默认为id selectById就是基于这个id,select 查询字段 from user where id ? 自定义告诉mybatisplus数据库的表名&#xff0c…

SpringMVC10、拦截器

10、拦截器 10.1、概述 SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。开发者可以自己定义一些拦截器来实现特定的功能。 过滤器与拦截器的区别:拦截器是AOP思想的具体应用。 过滤器 servlet规范中的一部分&…

AWS Database Migration Service 助力数据库搬迁

在业务出海的过程中,少不了的就是云迁移,但在云迁移的过程中其中最重要的一环就是数据库。通常迁移的成功就取决于数据,如果应用成功搬迁,数据库没过来也是无用功。因此如何快速、安全的进行数据库搬迁也成为一大难题。九河云公司…

8.5 Springboot项目实战 Redis缓存热点数据

文章目录 前言一、缓存与数据库一致性二、Repository层 -- Cache Aside模式实操BookRepositoryBookRepositoryImpl2.1 查询2.2 修改2.3 删除2.4 扩展Mapper修改三、Service层调用修改四、测试前言 前两文我们讲解了如何在SpringBoot中整合Redis,接下来我们将进行

智慧水务大数据,信息化云平台建设,综合运营管理平台

一、水务信息化的建设方向 1、完善基础设施构建软件定义的数据中心 基础设施,建设新一代软件定义的数据中心;逐步整合水务和海洋资源、统一、规范化各业务系统,按照一体化、一站式的服务进行建设。 2、整合信息资源建立智慧决策体系 统一信息采集方式,…

STM32CubeIDE基础学习-STM32CubeIDE软件工程文件拷贝粘贴

STM32CubeIDE基础学习-STM32CubeIDE软件工程文件拷贝粘贴 前言 在后面开发程序时,往往不需要再重新新建工程的了,可以直接在原有的工程基础上直接复制粘贴新增功能就可以了。 具体的操作方法步骤如下介绍: 第一步:找到一个原有的…

中医中的虚是什么?虚的分类?如何治疗气虚?

目录 中医中虚的分类气虚(此处指脾气虚)症状舌象:舌头中间区域有裂痕 或者 舌胖有齿痕身体上 解决方法其它脏腑气虚的情况及解决方法 血虚津液不足阴虚阳虚 中医中虚的分类 图片来源于微信视频的名为 路广林主任 的视频。 气虚(此…