虽然 Node.js 是单线程的,但是在融合了[libuv](https://github.com/libuv/libuv)后,使其有能力非常简单地就构建出高性能和可扩展的网络应用程序。
  下图是 Node.js 的简单架构图,基于 V8 和 libuv,其中 Node Bindings 为 JavaScript 和 C++ 搭建了一座沟通的桥梁,使得 JavaScript 可以访问 V8 和 libuv 向上层提供的 API。
:-: ![](https://img.kancloud.cn/52/6d/526d4497ed2e7c4d8fa7cb56b27adfd7_714x298.png =600x)
  本系列所有的示例源码都已上传至Github,[点击此处](https://github.com/pwstrick/node)获取。
## 一、术语解析
  接下来会对几个与 Node.js 相关的术语做单独的解析,其中事件循环会单独细讲。
**1)libuv**
  libuv 是一个事件驱动、非阻塞异步的 I/O 库,并且具备跨平台的能力,提供了一套事件循环(Event Loop)机制和一些核心工具,例如定时器、文件访问、线程池等。
**2)非阻塞异步的I/O**
  非阻塞是指线程不会被操作系统挂起,可以处理其他事情。
  异步是指调用者发起一个调用后,可以立即返回去做别的事。
  I/O(Input/Output)即输入/输出,通常指数据在存储器或其他周边设备之间的输入和输出。
  它是信息处理系统(例如计算机)与外部世界(可能是人类或另一信息处理系统)之间的通信。
  将这些关键字组合在一起就能理解 Node.js 的高性能有一部分是通过避免等待 I/O(读写数据库、文件访问、网络调用等)响应来实现的。
**3)事件驱动**
  事件驱动是一种异步化的程序设计模型,通过用户动作、操作系统或应用程序产生的事件,来驱动程序完成某个操作。
  在 Node.js 中,事件主要来源于网络请求、文件读写等,它们会被事件循环所处理。
  在浏览器的 DOM 系统中使用的也非常广泛,例如为按钮绑定 click 事件,在用点击按钮时,弹出提示或提交表单等。
**4)单线程**
  Node.js 的单线程是指运行 JavaScript 代码的主线程,网络请求或异步任务等都交给了底层的线程池中的线程来处理,其处理结果再通过事件循环向主线程告知。
  单线程意味着所有任务需要排队有序执行,如果出现一个计算时间很长的任务,那么就会占据主线程,其他任务只能等待,所以说 Node.js 不适合 CPU 密集型的场景。
   经过以上术语的分析可知,Node.js 的高性能和高并发离不开异步,所以有必要深入了解一下 Node.js 的异步原理。
# 二、事件循环
  当 Node.js 启动时会初始化事件循环,这是一个无限循环。
  下图是事件循环的一张运行机制图,新任务或完成 I/O 任务的回调,都会添加到事件循环中。
:-: ![](https://img.kancloud.cn/52/a4/52a4eca858c57f37c1eeea0f825c2fbd_1131x634.png =600x)
  下面是按照运行优先级简化后的[六个循环阶段](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)。
~~~
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
~~~
  每个阶段都有一个 FIFO 回调队列,当队列耗尽或达到回调上限时,事件循环将进入下一阶段,如此往复。
1. timers:执行由 setTimeout() 和 setInterval() 安排的回调。在此阶段内部,会维护一个定时器的小顶堆,按到期时间排序,先到期的先运行。
2. pending callbacks:处理上一轮循环未执行的 I/O 回调,例如网络、I/O 等异常时的回调。
3. idle,prepare:仅 Node 内部使用。
4. poll:执行与 I/O 相关的回调,除了关闭回调、定时器调度的回调和 setImmediate() , 适当的条件下 Node 将阻塞在这里。
5. check:调用 setImmediate() 回调。
6. close callbacks:关闭回调,例如 socket.on("close", callback)。
  在[deps/uv/src/unix/core.c](https://github.com/nodejs/node/blob/master/deps/uv/src/unix/core.c)文件中声明了事件循环的核心代码,旁边还有个 win 目录,应该就是指 Windows 系统中 libuv 相关的处理。
  其实事件循环就是一个大的 while 循环 ,具体如下所示。
  代码中的 UV\_RUN\_ONCE 就是上文 poll 阶段中的适当的条件,在每次循环结束前,执行完 close callbacks 阶段后,会再执行一次已到期的定时器。
~~~
static int uv__loop_alive(const uv_loop_t* loop) {
return uv__has_active_handles(loop) ||
uv__has_active_reqs(loop) ||
loop->closing_handles != NULL;
}
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
// 检查事件循环中是否还有待处理的handle、request、closing_handles是否为NULL
r = uv__loop_alive(loop);
// 更新事件循环时间戳
if (!r)
uv__update_time(loop);
// 启动事件循环
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop); // timers阶段,执行已到期的定时器
ran_pending = uv__run_pending(loop); // pending阶段
uv__run_idle(loop); // idle阶段
uv__run_prepare(loop);// prepare阶段
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout); // poll阶段
/* Run one final update on the provider_idle_time in case uv__io_poll
* returned because the timeout expired, but no events were received. This
* call will be ignored if the provider_entry_time was either never set (if
* the timeout == 0) or was already updated b/c an event was received.
*/
uv__metrics_update_idle_time(loop);
uv__run_check(loop); // check阶段
uv__run_closing_handles(loop); // close阶段
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop); // 执行已到期的定时器
}
r = uv__loop_alive(loop);
// 在 UV_RUN_ONCE 和 UV_RUN_NOWAIT 模式中,跳出当前循环
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0; // 标记当前的 stop_flag 为 0,表示跑完这轮,事件循环就结束了
return r;
}
~~~
**1)setTimeout 和 setImmediate**
  setTimeout 会在最前面的 timers 阶段被执行,而 setImmediate 会在 check 阶段被执行。
  但在下面的示例中,timeout 和 immediate 的打印顺序是不确定的。
  在 setTimeout()[官方文档](https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#settimeoutcallback-delay-args)中曾提到,当延迟时间大于 2147483647(24.8天) 或小于 1 时,将默认被设为 1。
  所以下面的 setTimeout(callback, 0) 相当于 setTimeout(callback, 1)。
  虽然在源码中会先运行 uv\_\_run\_timers(),但是由于上一次的循环耗时可能超过 1ms,也可能小于 1ms,所以定时器有可能还未到期。
  如此的话,就会造成打印顺序的不确定性,上述分析过程[参考了此处](https://cnodejs.org/topic/57d68794cb6f605d360105bf#57d7b1f53f3cb94e6b326746)。
~~~
setTimeout(() => {
console.log('timeout')
}, 0);
setImmediate(() => {
console.log('immediate')
});
~~~
  如果将 setTimeout() 和 setImmediate() 注册到 I/O 回调中运行,那么顺序就是确定的,先 immediate 再 timeout。
~~~
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
});
~~~
  这是因为 readFile() 的回调会在 poll 阶段运行,而在 uv\_\_io\_poll() 之后,就会立即执行 uv\_\_run\_check(),从而就能保证先打印 immediate 。
  在自己的日常工作中,曾使用过一个基于 setTimeout() 的定时任务库:[node-schedule](https://github.com/node-schedule/node-schedule)。
  由于延迟时间最长为 24.8 天,所以该库巧妙的运用了一个递归来弥补时间的上限。
~~~
Timeout.prototype.start = function() {
if (this.after <= TIMEOUT_MAX) {
this.timeout = setTimeout(this.listener, this.after)
} else {
var self = this
this.timeout = setTimeout(function() {
self.after -= TIMEOUT_MAX
self.start()
}, TIMEOUT_MAX)
}
if (this.unreffed) {
this.timeout.unref()
}
}
~~~
**2)与浏览器中的事件循环的差异**
  在浏览器的事件循环中,没有那么细的循环阶段,不过有两个非常重要的概念,那就是宏任务和微任务。
  宏任务包括 setTimeout()、setInterval()、requestAnimationFrame、Ajax、fetch()、脚本标签代码等。
  微任务包括 Promise.then()、MutationObserver。
  在 Node.js 中,[process.nextTick()](https://nodejs.org/dist/latest-v18.x/docs/api/process.html#processnexttickcallback-args)是微任务的一种,setTimeout()、setInterval()、setImmediate() 等都属于宏任务。
  在 Node版本 < 11 时,执行完一个阶段的所有任务后,再执行process.nextTick(),最后是其他微任务。
  可以这样理解,process.nextTick() 维护了一个独立的队列,不存在于事件循环的任何阶段,而是在各个阶段切换的间隙执行。
  即从一个阶段切换到下个阶段前执行,执行时机如下所示。
~~~
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘
nextTickQueue nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ poll │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ check │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
~~~
  但是在 Node 版本 >= 11 之后,会处理的和浏览器一样,也是每执行完一个宏任务,就将其微任务也一并完成。
  在下面这个示例中, setTimeout() 内先声明 then(),再声明 process.nextTick(),最后执行一条打印语句。
  接着在 setTimeout() 之后再次声明了 process.nextTick()。
~~~
// setTimeout
setTimeout(() => {
Promise.resolve().then(function() {
console.log('promise');
});
process.nextTick(() => {
console.log('setTimeout nextTick');
});
console.log('setTimeout');
}, 0);
// nextTick
process.nextTick(() => {
console.log('nextTick');
});
~~~
  我本地运行的 Node 版本是 16,所以最终的打印顺序如下所示。
~~~
nextTick
setTimeout
setTimeout nextTick
promise
~~~
  外面的 process.nextTick() 要比 setTimeout() 先运行,里面的打印语句最先执行,然后是 process.nextTick(),最后是 then()。
**3)sleep()**
  有一道比较经典的题目是编写一个 sleep() 函数,实现线程睡眠,在日常开发中很容易就会遇到。
  搜集了多种实现函数,有些是同步,有些是异步。
  第一种是同步函数,创建一个循环,占用主线程,直至循环完毕,这种方式也叫循环空转,比较浪费CPU性能,不推荐。
~~~
function sleep(ms) {
var start = Date.now(), expire = start + ms;
while (Date.now() < expire);
}
~~~
  第二至第四种都是异步函数,本质上线程并没有睡眠,事件循环仍在运行,下面是 Promise + setTimeout() 组合实现的 sleep() 函数。
~~~
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
~~~
  第三种是利用 util 库的[promisify()](https://nodejs.org/dist/latest-v18.x/docs/api/util.html#utilpromisifyoriginal)函数,返回一个 Promise 版本的定时器。
~~~
function sleep(ms) {
const { promisify } = require('util');
return promisify(setTimeout)(ms);
}
~~~
  第四种是当 Node 版本 >= 15 时可以使用,在[timers库](https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#timerspromisessettimeoutdelay-value-options)中直接得到一个 Promise 版本的定时器。
~~~
function sleep(ms) {
const { setTimeout } = require('timers/promises');
return setTimeout(ms);
}
~~~
  第五种是同步函数,可利用[Atomics.wait](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait)阻塞事件循环,直至线程超时,实现细节在此不做说明了。
~~~
function sleep(ms) {
const sharedBuf = new SharedArrayBuffer(4);
const sharedArr = new Int32Array(sharedBuf);
return Atomics.wait(sharedArr, 0, 0, ms);
}
~~~
  还可以编写 C/C++ 插件,直接调用操作系统的 sleep() 函数,此处不做展开。
参考资料:
[Event Loop](https://mp.weixin.qq.com/s/RNYYNR7A01V-Y2aC1wNsGw) [事件循环源码](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter5/chapter5-1.html) [Node.js技术栈](https://www.nodejs.red/#/nodejs/translate/everything-you-need-to-know-about-node-js-lnc?id=the-event-loop%ef%bc%88%e4%ba%8b%e4%bb%b6%e5%be%aa%e7%8e%af%ef%bc%89)
[nodejs真的是单线程吗?](https://segmentfault.com/a/1190000014926921)
[Nodejs探秘:深入理解单线程实现高并发原理](https://imweb.io/topic/5b6cf97093759a0e51c917c8)
[什么是CPU密集型、IO密集型?](https://zhuanlan.zhihu.com/p/62766037) [libuv](https://luohaha.github.io/Chinese-uvbook/source/introduction.html) [I/O](https://zh.m.wikipedia.org/zh-cn/I/O)
[JavaScript 运行机制详解:再谈Event Loop](https://www.ruanyifeng.com/blog/2014/10/event-loop.html)
[Node.js Event Loop 的理解 Timers,process.nextTick()](https://cnodejs.org/topic/57d68794cb6f605d360105bf)
[浏览器与Node的事件循环(Event Loop)有何区别?](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/26)
[Why is the EventLoop for Browsers and Node.js Designed This Way?](https://blog.bitsrc.io/why-is-the-eventloop-for-browsers-and-node-js-designed-this-way-f7f794696c?gi=29723793aa09)
[Node.js 事件循环](https://learnku.com/articles/38802) [Phases of the Node JS Event Loop](https://medium.com/@kunaltandon.kt/process-nexttick-vs-setimmediate-vs-settimeout-explained-wrt-different-event-loop-phases-c0506b12921d)
[如何实现线程睡眠?](https://www.nodejs.red/#/nodejs/tips/sleep?id=%e4%ba%8c%ef%bc%9a%e5%ae%9a%e6%97%b6%e5%99%a8-promise-%e5%ae%9e%e7%8e%b0-sleep)
[nodejs中的并发编程](https://segmentfault.com/a/1190000022113106)
*****
> 原文出处:
[博客园-Node.js精进](https://www.cnblogs.com/strick/category/2154090.html)
[知乎专栏-前端性能精进](https://www.zhihu.com/column/c_1611672656142725120)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎浏览。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020