>[info]部分内容来源:《新时期的Node.js入门》- 李锴
推荐阅读:
[https://www.zhihu.com/question/327657434/answer/715340900](https://www.zhihu.com/question/327657434/answer/715340900)
[https://elemefe.github.io/node-interview/#/sections/zh-cn/](https://elemefe.github.io/node-interview/#/sections/zh-cn/)
[TOC]
# 底层机制
## 单线程与多线程
其他语言(如 Java、C++等)都有多线程的语言特性,即开发者可以派生出多个线程来协同工作;Node 并没有提供多线程的机制,开发者无法在一个独立进程中增加新的线程,但是可以派生出多个进程来达到并行完成工作的目的。
Node 的底层实现(C++)并非是单线程的,libuv 会通过类似线程池的实现来模拟不同操作系统下的异步调用,这对开发者来说是不可见的。
<span style="color: MediumOrchid;font-size: 20px;">Libuv 中的多线程</span>
![](https://box.kancloud.cn/2cd0797877324f39d617015066fb1cb5_637x300.png =450x)
Libuv 是一个跨平台的异步 IO 库,它结合了 UNIX 下的 libev 和 Windows下的 IOCP 的特性,最早由 Node 的作者开发,专门为 Node 提供多平台下的异步 IO 支持。Node 中的非阻塞 IO 以及事件循环的底层机制,都是由 libuv 来实现的。
在 Windows 环境下,libuv直接使用 Windows 的 IOCP(I/O Completion Port)来实现异步 IO。在非 Windows 环境下,libuv 使用多线程来模拟异步 IO。
以 readFile 为例,读取文件的系统调用是由 libuv 来完成的,Node 只负责调用 libuv 的接口,等数据返回后再执行对应的回调方法。
## 并行与并发
首先搞清楚这两个概念的差别:这里以一个排队取火车票的场景来进行介绍
并发(Concurrent):并发是假设有两个队伍但只有 1 个取票机,两个队伍轮流取票。
并行(Parallel):并行是假设有两个队伍,不同的是开放了 2 个取票机,那么这两个队列可以同时向前移动。
>以操作系统的角度:并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。操作系统通过引入进程和线程,使得程序能够并发运行。
Node 中的并发:单线程支持高并发,通常是依靠异步+事件循环来实现的,异步使得代码能在面临多个请求时不会发生阻塞,事件循环提供了 IO 调用结束后调用回调函数的能力。
>[success]多个请求同时到达时,Java、C++ 等能通过开多个线程来处理请求,而 node.js 却不能开多线程,其利用事件驱动和异步 I/O 的特性极大地提升了程序性能从而使其有能力这种高并发场景。具体的解释可以参考《深入浅出 node.js》笔记部分。
## Node中的事件循环(event loop)
浏览器的事件循环中提到了宏任务和微任务,在 node.js 中,事件循环被分成了 6 个不同的阶段。**每个阶段执行特定的宏任务,每执行完一个阶段的回调,就清空一次微任务队列。**
宏任务包括 `script , setTimeout , setInterval , setImmediate , I/O , UI rendering`
微任务包括` process.nextTick, promise.then, Object.observe , MutationObserver`
[MutationObserver使用方式见这里](http://javascript.ruanyifeng.com/dom/mutationobserver.html)
*****
![](https://box.kancloud.cn/6cd4a0b75719a1aac894564bcfafcb2d_430x411.png)
* timers: 用于处理 setTimeout() 和 setInterval() 的回调。
* I/O callbacks: 大多数的回调方法会在这个阶段执行,除了 timers、close 和 setImmediate
* idle, prepare: 仅内部使用。
* poll: 轮训,不断检查有没有新的 I/O 事件
* check: 执行 setImmediate() 事件的回调。
* close callbacks: 处理一些 close 相关的事件,例如 socket.on('close',...)
<span style="color: MediumOrchid;font-size: 20px;">process.nextTick 与 setImmediate</span>
`setImmediate`接受一个回调函数作为参数,其不像 setTimeout 一样可以设置定时器的时间,事件循环的整个 check 阶段是为 setImmediate 方法而设置的:一般情况下,当事件循环到达 poll 阶段后,就会检查当前代码是否调用了 setImmediate,如果一个回调函数是被 setImmediate 方法调用的,事件循环就会跳出 poll 阶段而进入 check 阶段。
`process.nextTick`接受一个回调函数作为参数,该方法定义的回调方法会被加入到名为 nextTickQueue 的队列中,在事件循环的任何阶段,如果 nextTickQueue 不为空,都会在当前阶段操作结束后优先执行 nextTickQueue 中的回调函数。
>网上有的文章说 process.nextTick 是 idle 观察者,setImmediate 属于 check 观察者(《深入浅出 node.js 》里也是这么说的,不过那也是好几年前了),这个实际上与 node 的版本有关,一般也不用特别纠结。可以阅读 [这篇帖子](https://segmentfault.com/q/1010000011914016/a-1020000011915491)
>[warning]看上去似乎和微任务的执行是类似的,那么 promise.then 和 process.nextTick 的回调谁更先执行呢?setImmediate 和 nextTick 的出现是为了解决什么问题呢?
```js
let promise = new Promise((resolve, reject) => {
resolve()
})
promise.then(() => {
console.log('promise')
})
process.nextTick(function () {
console.log('nextTick')
})
// 打印顺序:nextTick promise 无论 Promise 放在之前还是之后都是一样
```
看上去似乎是 nextTick 的回调比 promise.then 的回调优先级更高
*****
TODO
- node 事件循环案例
- node 事件循环补充要点
*****
# 常用模块(原生node.js)
## Buffer
Buffer 是 node.js 特有的(区别于浏览器 JavaScript)的数据类型,主要用来处理二进制数据。前后端通信时二进制数据流十分常见(例如传输一张 gif 图片)。Buffer 属于固有(built-in)类型,因此无需 require 引入。
在文件操作和网络操作中,如果不显式声明编码格式,其返回数据的默认类型就是 Buffer。
**拼接Buffer**
```
var data = []
res.on('data', function (chunk) {
data.push(chunk)
})
res.on('end', function () {
var buf = Buffer.concat(data)
console.log(buf.toString())
})
```
更多与 Buffer 相关的内容见《深入浅出 node.js》部分
## 文件系统(File System)
该模块提供了读写文件的能力,借助于底层的 linux 的 C++ API 来实现,这些 API 大多提供同步和异步两种版本。下面仅介绍几个常用的。
- `fs.readFile(file[, options], callback)`
readFile 方法用于异步读取文本文件中的内容,适用于体积较小的文件,数百 MB 的文件建议使用 stream。readFile 读出的数据需要在回调方法中获取,而 readFileSync 直接返回文本数据内容
```js
const fs = require('fs')
fs.readFile('foo.txt', function (err, data) {
if (err) throw err
console.log(data)
})
// 如果不指定readFile的coding配置,其会返回Buffer格式
// <Buffer 48 65 .. .. ..>
const data = fs.readFileSync('foo.txt', {encoding: 'UTF-8'})
```
- `fs.writeFile(file, data[, options], callback)`
fs.writeFile('文件路径','要写入的内容',['编码'],'回调函数')
写入时如果文件不存在,则会尝试创建它,默认的 flag 为 ’w’,r 代表读取文件,w 代表写文件,a 代表追加。
[https://www.cnblogs.com/starof/p/5038300.html](https://www.cnblogs.com/starof/p/5038300.html)
```js
fs.writeFile(filePath, mp3, 'binary', err => {
if (err) {
res.json({
error: 1,
msg: '下载失败'
})
} else {
res.json({
error: 0,
msg: '下载成功',
path: downloadUrl
})
}
})
```
- `fs.stat(path, callback)`
stat 方法通常用于获取文件的状态,通常开发者可以在调用 open()、read()、或者 write 方法之前调用 fs.stat 方法,用来判断文件是否存在
如果文件存在,则返回文件的状态信息,如下
```shell
Stats {
dev: 215266619,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: undefined,
ino: 2251799814181389,
size: 2023,
blocks: undefined,
atimeMs: 1557839146143.9646,
mtimeMs: 1559027178498.3296,
ctimeMs: 1559027178498.3296,
birthtimeMs: 1557839146143.9646,
atime: 2019-05-14T13:05:46.144Z,
mtime: 2019-05-28T07:06:18.498Z,
ctime: 2019-05-28T07:06:18.498Z,
birthtime: 2019-05-14T13:05:46.144Z }
```
如果文件不存在,则会出现 `Error: ENOENT: no such file or directory` 的错误
**实例:获取目录下所有的文件名**
这是一个常见的需求,实现这个功能只需要 fs.readdir 以及 fs.stat 两个 API,readdir 用于获取目录下的所有文件或者子目录,stat 用于判断具体每条记录时文件还是子目录,如果是子目录,则递归调用整个方法
```
const fs = require('fs')
function getAllFileFromPath(path) {
fs.readdir(path, function (err, res) {
for (let subPath of res) {
// 这里使用同步方法而不是异步
let statObj = fs.statSync(path + '/' + subPath)
if (statObj.isDirectory()) { // 判断是否为目录
console.log('Dir: ', subPath)
// 如果是文件夹,递归获取子目录中的文件列表
} else {
console.log('File: ', subPath)
}
}
})
}
getAllFileFromPath(__dirname)
```
## HTTP 服务
HTTP 模块提供了一系列用于网络传输的 API,这些 API 大都位于比较底层的位置,可以让开发者自由控制整个 HTTP 传输过程
在 HTTP 模块中,node 定义了一些顶级的类、属性以及方法,如下所示
*****
Class: http.Agent
Class: http.ClientRequest
Class: http.Server
Class: http.ServerResponse
Class: http.IncomingMessage
http.METHODS
http.STATUS_CODES
http.createClient([port], [, host])
http.createServer([requestListener])
http.get(options[, callback])
http.request(options[, callback])
......
*****
### 创建 HTTP 服务
```
const http = require('http')
const server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'})
res.end('Hello World!')
})
server.listen(3000)
```
上面的代码使用 createServer 方法创建了一个简单的 HTTP 服务器,该方法返回一个 http.server 类的实例,createServer 方法包含了一个匿名的回调函数,该函数有两个参数 req 和 res,它们分别是 IncomingMessage 和 ServerResponse 的实例。分别表示 HTTP 的 request 和 response 对象,服务器创建完成后,node 进程开始循环监听 3000 端口(由listen方法实现)
### 处理 HTTP 请求
**request 对象**
*****
**1.method、URL**
```
const method = req.method
const url = req.url
http://example.com/index.html?nam=Lear
-> url: /index.html?name=Lear
```
**2.header**
http header 通常为以下的形式
```
{
'content-length': '123',
'content-type': 'text/plain',
'connection': 'keep-alive',
'host': 'mysite.com'
...
}
```
通过 req.headers 获取一个 JSON 对象,可以对属性名进行单独索引
```
const headers = req.headers
const userAgent = headers['user-agent']
```
**3.request body**
node 通过 stream 来处理 HTTP 的请求体,stream 注册了 data 和 end 两个事件,可以用如下的代码来获取完整的 HTTP 请求体
```
let body = []
request.on('data', chunk => {
body.push(chunk)
}).on('end', () => {
body = Buffer.concat(body).toString()
})
```
**response对象**
*****
**1.设置 response header**
通过`setHeader`方法可以设置 response 的头部信息
```js
response.setHeader('Content-Type': 'application/json')
```
`setHeader` 方法只能设置 response header 单个属性的内容,如果想要一次性设置所有的响应头和状态码,可以使用`writeHead`方法
`writeHead` 方法用于定义 HTTP 响应头,包括状态码等一系列属性
```js
response.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain'
})
```
调用该方法后,服务端向客户端发送 HTTP 响应头,后面通常会跟着调用 `res.write` 等方法,响应头不可重复发送;有时开发者并不会显式调用该方法,调用 `res.end` 方法也会调用`writeHead`方法,此时 statusCode 会自动设置为 200
**2.response body**
response 对象是一个 writableStream 实例,可以直接调用 write 方法进行写入,写入完成后,再调用 end 方法将该 stream 发送到客户端
```js
response.write('<html>')
response.write('<body>')
response.write('<h1>Hello World!</h1>')
response.write('</body>')
response.write('</htnl>')
response.end()
// 或者写成这样:直接将 response body 作为 end 方法的参数返回
response.end('<html><body><h1>Hello World!</h1></body></html>')
```
**3.response.end**
end 方法在每个 HTTP 请求的最后都会被调用,开发者应该调用该方法来结束 HTTP 请求。如果不调用 end 方法,浏览器地址栏左边的叉号会一直存在,表示该请求尚未完成。
end 方法支持一个字符串或者 buffer 作为参数,可以指定在 HTTP 请求的最后返回的数据,该数据会在浏览器页面上显示出来;如果定义了回调方法,那么会在 end 返回后调用
```
res.end('Hello node', () => {
console.log('http cycle end')
})
```
### Stream
文件系统提供的 API 有一个重要的问题,就是如果文件过大,一次性地读或写显然是不可行的,Stream 允许我们把较大的数据分批次地进行读写。
在 node.js 中,一共有四种基础的 stream 类型:
- Readable:可读流(for example fs.createReadStream())
- Writable:可写流(for example fs.createWriteStream())
- Duplex:既可读,又可写(for example net.Socket)
- Transform:操作写入的数据,然后读取结果,通常用于输入数据和输出数据不要求匹配的场景,如 zlib.createDeflate()
```js
// 复制文件
const fs = require('fs')
const path = require('path')
const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
const readStream = fs.createReadStream(fileName1)
const writeStream = fs.createWriteStream(fileName2)
readStream.pipe(writeStream)
readStream.on('data', chunk => {
console.log(chunk)
console.log(chunk.toString())
})
readStream.on('end', () => {
console.log('copy done')
})
```
```
// 也可用于http
const http = require('http')
const fs = require('fs')
const path = require('path')
const fileName1 = path.resolve(__dirname, 'data.txt')
const server = http.createServer((req, res) => {
if (req.method === 'GET') {
const readStream = fs.createReadStream(fileName1)
readStream.pipe(res)
}
})
```
Writable Stream 主要使用 write 方法来写入数据,该方法是异步的,假设我们创建一个可读流读取一个较大的文件,再调用 pipe 方法将数据通过一个可写流写入另一个位置。如果读取的速度大于写入的速度,那么 node 将会在内存中缓存这些数据。
当然缓冲区也是有大小限制的(state.highWatermark),当达到阈值后,write方法会返回 false,可读流也进入暂停状态,当 writable stream 将缓冲区清空之后,会触发 drain 事件,上游的 readable 重新开始读取数据
pipe 方法使得数据可以通过管道**由可读流流入可写流**。pipe 方法接收一个 writable 对象,当 readable 对象调用 pipe 方法时,会在内部调用 writable 对象的 write 方法进行写入。
### ReadLine
ReadLine 是一个 node 原生模块,提供了按行读取 Stream 中数据的功能
```
const readline = require('readline')
const fs = require('fs')
const rl = readline.createInterface({
input: fs.createReadStream('data.txt')
})
// 每读取一行数据出发
rl.on('line', data => {
console.log(data)
})
rl.on('close', () => {
console.log('closed')
})
```
### Events
node 程序中的对象会产生一系列的事件,例如一个 HTTP Server 会在每次有新连接时触发一个事件,一个 Readable Stream 会在文件打开时触发一个事件等。所有能触发事件的对象都是 EventEmitter 类的实例。下面的代码演示了如何注册一个事件并触发它:
```js
const eventEmitter = require('events')
const myEmitter = new eventEmitter()
myEmitter.on('begin', () => {
console.log('begin')
})
myEmitter.emit('begin')
```
在实际的开发中,通常不会直接使用 Event 模块来进行事件处理,而是选择将其作为基类进行继承的方式来使用 Event,在 node 的内部实现中,凡是提供了事件机制的模块,都会在内部继承 Event 模块,以 fs 模块为例,下面是其源码中的一部分
```
function FSWatcher() {
EventEmitter.call(this)
// ......
}
util.inherits(FSWatcher, EventEmitter) // util.inherits 是用来继承的方法
```
假设我们要用 node 来开发一个网页上的音乐播放器,关于播放和暂停的处理,就可以考虑通过继承 Events 模块来实现:
```
const util = require('util')
const event = require('events')
function Player() {
event.call(this)
}
util.inherits(Player, event)
const player = new Player()
player.on('pause', () => {
console.log('pause')
})
player.on('play', () => {
console.log('playing')
})
player.emit('play') // playing
```
### path 模块
原文链接:[https://www.jianshu.com/p/78fadd20ee61](https://www.jianshu.com/p/78fadd20ee61)
1.连接路径:path.join(\[path1\]\[, path2\]\[, ...\])
path.join() 方法可以连接任意多个路径字符串。要连接的多个路径可做为参数传入。path.join() 方法在接边路径的同时也会对路径进行规范化。例如:
```js
var path = require('path')
// 合法的字符串连接
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..')
// 连接后
'/foo/bar/baz/asdf'
//不合法的字符串将抛出异常
path.join('foo', {}, 'bar')
// 抛出的异常
TypeError: Arguments to path.join must be strings'
```
2.路径解析:path.resolve(\[from ...\], to)
path.resolve() 方法可以将多个路径解析为一个规范化的绝对路径。其处理方式类似于对这些路径逐一进行 cd 操作,与 cd 操作不同的是,这些路径可以是文件,并且可不必实际存在(resolve() 方法不会利用底层的文件系统判断路径是否存在,而只是进行路径字符串操作)。例如:
```js
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
```
相当于
```shell
cd foo/bar
cd /tmp/file/
cd ..
cd a/../subfile
pwd
```
示例:
```js
path.resolve('/foo/bar', './baz')
// 输出结果为
'/foo/bar/baz'
path.resolve('/foo/bar', '/tmp/file/')
// 输出结果为
'/tmp/file'
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 当前的工作路径是 /home/itbilu/node,则输出结果为
'/home/itbilu/node/wwwroot/static_files/gif/image.gif'
```
3.对比
```js
const path = require('path');
let myPath = path.join(__dirname,'/img/so');
let myPath2 = path.join(__dirname,'./img/so');
let myPath3 = path.resolve(__dirname,'/img/so');
let myPath4 = path.resolve(__dirname,'./img/so');
console.log(__dirname); // D:\myProgram\test
console.log(myPath); // D:\myProgram\test\img\so
console.log(myPath2); // D:\myProgram\test\img\so
console.log(myPath3); // D:\img\so
console.log(myPath4); // D:\myProgram\test\img\so
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直线、矩形、多边形
- Part2-曲线图形
- Part3-线条操作
- Part4-文本操作
- Part5-图像操作
- Part6-变形操作
- Part7-像素操作
- Part8-渐变与阴影
- Part9-路径与状态
- Part10-物理动画
- Part11-边界检测
- Part12-碰撞检测
- Part13-用户交互
- Part14-高级动画
- CSS
- SCSS
- codePen
- 速查表
- 面试题
- 《CSS Secrets》
- SVG
- 移动端适配
- 滤镜(filter)的使用
- JS
- 基础概念
- 作用域、作用域链、闭包
- this
- 原型与继承
- 数组、字符串、Map、Set方法整理
- 垃圾回收机制
- DOM
- BOM
- 事件循环
- 严格模式
- 正则表达式
- ES6部分
- 设计模式
- AJAX
- 模块化
- 读冴羽博客笔记
- 第一部分总结-深入JS系列
- 第二部分总结-专题系列
- 第三部分总结-ES6系列
- 网络请求中的数据类型
- 事件
- 表单
- 函数式编程
- Tips
- JS-Coding
- Framework
- Vue
- 书写规范
- 基础
- vue-router & vuex
- 深入浅出 Vue
- 响应式原理及其他
- new Vue 发生了什么
- 组件化
- 编译流程
- Vue Router
- Vuex
- 前端路由的简单实现
- React
- 基础
- 书写规范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 与 Hook
- 《深入浅出React和Redux》笔记
- 前半部分
- 后半部分
- react-transition-group
- Vue 与 React 的对比
- 工程化与架构
- Hybird
- React Native
- 新手上路
- 内置组件
- 常用插件
- 问题记录
- Echarts
- 基础
- Electron
- 序言
- 配置 Electron 开发环境 & 基础概念
- React + TypeScript 仿 Antd
- TypeScript 基础
- 样式设计
- 组件测试
- 图标解决方案
- Algorithm
- 排序算法及常见问题
- 剑指 offer
- 动态规划
- DataStruct
- 概述
- 树
- 链表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 课程实战记录
- 服务器
- 操作系统基础知识
- Linux
- Nginx
- redis
- node.js
- 基础及原生模块
- express框架
- node.js操作数据库
- 《深入浅出 node.js》笔记
- 前半部分
- 后半部分
- 数据库
- SQL
- 面试题收集
- 智力题
- 面试题精选1
- 面试题精选2
- 问答篇
- Other
- markdown 书写
- Git
- LaTex 常用命令