介绍 根据 Node.js 官网的定义:Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。 Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
Node.js 是基于 Chrome V8 引擎构建的.由事件循环分发 I/O 任务,最终工作线程将任务丢进线程池中去执行,而事件循环只要等待执行结果就可以了.
异步 IO 重复调用 IO 操作,判断是否完成,称为轮询.包括read
,select
,poll
,kqueue
,event ports
. 期望实现无须主动判断的非阻塞 I/O.
异步 IO 优点
前端通过异步 IO 可以消除阻塞。
请求耗时少,假如有两个请求 A 和 B,那么异步 IO 用时为:Max(A+B)。同步则为 A+B,请求越多差距越大。
IO 是昂贵的,分布式 IO 是更昂贵的。
Node.js 适用于 IO 密集型,而不适用于 CPU 密集型。
并不是所有都用异步任务好,遵循一个公式: s= (Ws+Wp)/(Ws+Wp/p) Ws 表示同步任务,Wp 表示异步任务,p 表示处理器的数量。
异步 IO 实现
应用程序先将 JS 代码经 V8 转换为机器码。
通过 Node.js Bindings 层,向操作系统 Libuv 的事件队列中添加一个任务。
Libuv 将事件推送到线程池中执行。
线程池执行完事件,返回数据给 Libuv。
Libuv 将返回结果通过 Node.js Bindings 返回给 V8。
V8 再将结果返回给应用程序。
libuv 多种异步 IO 实现的抽象封装层. libuv 实现了 Node.js 中 Eventloop,主要分为以下几个阶段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming : │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
上图中每一个阶段都有一个先进先出的回调队列,只有当队列内的事件执行完成之后,才会进入下一个阶段。
timers:执行 setTimeout
和 setInterval
中到期的 callback。
pending callbacks:上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。
执行一些系统操作的回调,例如 tcp 连接发生错误。
idle, prepare:仅内部使用。
poll:最为重要的阶段,执行 I/O callback(node 异步 api 的回调,事件订阅回调等),在适当的条件下会阻塞在这个阶段。
如果 poll 队列不为空,直接执行队列内的事件,直到队列清空。
如果 poll 队列为空。
如果有设置 setImmediate
,则直接进入 check 阶段。
如果没有设置 setImmediate
,则会检查是否有 timers 事件到期。
如果有 timers 事件到期,则执行 timers 阶段。
如果没有 timers 事件到期,则会阻塞在当前阶段,等待事件加入。
check:执行 setImmediate
的 callback。
close callbacks:执行 close 事件的 callback,例如 socket.on("close",func)
。
除此之外,Node.js 提供了 process.nextTick(微任务,promise 也一样) 方法,在以上的任意阶段开始执行的时候都会触发。
Event Loop 是一个很重要的概念,指的是计算机系统的一种运行机制。
Libuv 在 Linux 下基于 Custom Threadpool 实现。
Libuv 在 Windows 下基于 IOCP 实现。
常见的异步 IO 使用方式
使用 step,q,async 等异步控制库。
使用 Promise 处理异步。
使用 EventEmitter,实现“发布/订阅”模式处理异步。
Node.js 暂不支持协程,可使用 Generator 代替。
终极解决方案:async、await。
事件驱动架构 commonjs 模块 用法
引入模块:require(“./index.js”)
导出模块:module.exports={…} 或者 exports.key={}
注意:exports 为 module.exports 的引用,不可使用 exports 直接赋值,模块无法导出,eg:exports={}
缓存:require 值会缓存,通过 require 引用文件时,会将文件执行一遍后,将结果通过浅克隆的方式,写入全局内存,后续 require 该路径,直接从内存获取,无需重新执行文件
值拷贝:模块输出是值的拷贝,一但输出,模块内部变化后,无法影响之前的引用,而 ESModule 是引用拷贝。commonJS 运行时加载,ESModule 编译阶段引用
CommonJS 在引入时是加载整个模块,生成一个对象,然后再从这个生成的对象上读取方法和属性
ESModule 不是对象,而是通过 export 暴露出要输出的代码块,在 import 时使用静态命令的方法引用制定的输出代码块,并在 import 语句处执行这个要输出的代码,而不是直接加载整个模块
面试题:为何直接使用 exports 导出 commonjs 模块,eg:exports = “hello” 解析:commonjs 模块是通过 module.exports 导出模块,exports 为 module.exports 的引用,使用 exports 直接赋值导出模块,只会改变 exports 的引用不会导出 commonjs 模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 exports .key = "hello world" ;module .exports = "hello world" ;exports = "hello world" ; const obj = { key : {}, }; obj.key = "hello world" ; const key = obj.key ;key.key1 = "hello world" ; key = "hello world" ;
module 属性
任意 js 文件就是一个模块.可以直接使用 module 属性.
id: 返回模块标识符,一般是一个绝对路径
filename: 返回绝对路径
loaded: 返回布尔值,表示模块是否加载完成
parent: 返回对象存放调用当前模块的模块
children: 返回数组,存放当前模块调用的其他模块
exports;返回当前模块需要暴露的内容
paths: 返回数组,存放不同目录下的 node_modules 位置
原理实现 使用 fs、vm、path 内置模块,以及函数包裹形式实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const vm = require ("vm" );const path = require ("path" );const fs = require ("fs" );function customRequire (filename ) { const pathToFile = path.resolve (__dirname, filename); const content = fs.readFileSync (pathToFile, "utf-8" ); const wrapper = [ "(function(require, module, exports, __dirname, __filename){" , "})" , ]; const wrappedContent = wrapper[0 ] + content + wrapper[1 ]; const script = new vm.Script (wrappedContent, { filename : "index.js" , }); const module = { exports : {}, }; const result = script.runInThisContext (); result (customRequire, module , module .exports ); return module .exports ; } global .customRequire = customRequire;
源码分析
源码路径:/lib/internal/modules/cjs/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 1 、Module .prototype .require :调用__load函数2 、Module .__load :_cache处理,调用load函数3 、Module .prototype .load 函数:调用Module ._extensions [extension](this , filename);4 、Module ._extensions ['.js' ] = function (module , filename ) { if (cached?.source ) { content = cached.source ; cached.source = undefined ; } else { content = fs.readFileSync (filename, 'utf8' ); } module ._compile (content, filename); } 5 、Module .prototype ._compile = function (content, filename ){ const compiledWrapper = wrapSafe (filename, content, this ); const exports = this .exports ; const thisValue = exports ; const module = this ; if (inspectorWrapper) { result = inspectorWrapper (compiledWrapper, thisValue, exports , require , module , filename, dirname); } else { result = ReflectApply (compiledWrapper, thisValue, [exports , require , module , filename, dirname]); } return result; } 6 、function wrapSafe (filename, content, cjsModuleInstance ) { const wrapper = Module .wrap (content); return vm.runInThisContext (wrapper, { filename, lineOffset : 0 , displayErrors : true , importModuleDynamically : async (specifier) => { const loader = asyncESM.ESMLoader ; return loader.import (specifier, normalizeReferrerURL (filename)); }, }); }
1 2 3 4 5 6 7 8 9 10 const Module = require ("module" );Module ._extensions [".test" ] = Module ._extensions [".js" ];const prevFunc = Module ._extensions [".js" ];Module ._extensions [".js" ] = function (...args ) { console .log ("load script" ); prevFunc.apply (prevFunc, args); };
事件循环
同步代码
process.nextTick(优先级高于 promise)和 promise.then() ,之后进入事件循环
timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调
I/O callbacks 阶段 :处理一些上一轮循环中的少数未执行的 I/O 回调
idle, prepare 阶段 :仅 node 内部使用
poll 阶段 :获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
check 阶段 :执行 setImmediate() 的回调
close callbacks 阶段:执行 socket 的 close 事件回调
执行流程
执行同步代码
查看是否有满足条件的微任务,执行
执行 timer 中满足条件的宏任务
timer 中宏任务全部执行完毕切换队列
切换前会清空微任务
之后按照 timer => poll => check 检查是否有任务执行
和浏览器 eventloop 区别: 浏览器执行宏任务就会把本次循环生成的微任务清空,而 node 是在切换队列前清空,所以要将 timer 中宏任务全部执行完毕,再执行微任务. 习题解析:
new Promise(()=>{//同步执行}).then(()=>{//异步执行})
async function test(){console.log() //同步 -> await test(0) //同步 -> console.log()//异步}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 setTimeout (() => { console .log ("setTimeout" ); }, 0 ); setImmediate (() => { console .log ("setImmediate" ); }); setTimeout (() => { setTimeout (() => { console .log ("setTimeout2" ); }, 0 ); setImmediate (() => { console .log ("setImmediate" ); }); console .log ("setTimeout1" ); }, 0 ); setTimeout (() => { console .log ("timeout" ); }, 0 ); setImmediate (() => { console .log ("immediate" ); setImmediate (() => { console .log ("immediate1" ); }); new Promise ((resolve ) => { console .log (77 ); resolve (); }).then (() => { console .log (88 ); }); process.nextTick (function ( ) { console .log ("nextTick1" ); }); }); new Promise ((resolve ) => { console .log (7 ); resolve (); }).then (() => { console .log (8 ); }); process.nextTick (function ( ) { console .log ("nextTick2" ); }); console .log ("start" );
模块 全局对象 Node 中全局对象是global
,如果直接在模块中获取 this 指向的是{}
. 而模块最终会封装进自执行函数中,指向的是global
.
全局对象:global,下挂如下对象和函数,使用时无需模块引入
global
Buffer、process、console、queueMicrotask
setTimeout、clearTimeout、setInterval、setImmediate、clearInterval
模块中使用注入变量:
__filename
:当前文件名称带路径,eg:/Users/jian/workspace/cjs/index.js
__dirname
:当前文件夹路径,eg:/Users/jian/workspace/cjs/
cjs
:require, module, exports
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 > console .log (__dirname) > console .log (this ) console .log (this )console .log (__filename)console .log (__dirname)
process
argv:返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是 node,第二个成员是脚本文件名,其余成员是脚本文件的参数
env:返回一个对象,成员为当前 shell 的环境变量
stdin:标准输入流
stdout:标准输出流
cwd():返回当前进程的工作目录
1 2 3 4 console .log (process.argv );console .log (process.cwd ()); process.stdout .write ("Hello World!" );
底层依赖模块
V8 引擎:主要是 JS 语法的解析,有了它才能识别 JS 语法
libuv:C 语⾔实现的⼀个⾼性能异步⾮阻塞 IO 库,⽤来实现 node.js 的事件循环
http-parser/llhttp:底层处理 http 请求,处理报⽂,解析请求包等内容
openssl:处理加密算法,各种框架运⽤⼴泛,底层依赖,无需 js 实现
zlib:处理压缩等内容
Eventmitter
大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它,包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。
EventEmitter 会按照监听器注册的顺序同步地调用所有监听器,所以必须确保事件的排序正确,且避免竞态条件。
常见 api
addListener(event, listener)添加监听器
removeListener(event, listener)移除监听器
removeAllListeners([event])移除所有监听器
on(event, listener)注册监听器
off(eventName, listener)移除监听器
once(event, listener)为指定事件注册一个单次监听器
emit(event, [arg1], [arg2], […])按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false
错误处理:当 EventEmitter 实例出错时,应该触发 ‘error’ 事件。 这些在 Node 中被视为特殊情况。如果没有为 ‘error’ 事件注册监听器,则当 ‘error’ 事件触发时,会抛出错误、打印堆栈跟踪、并退出 Node.js 进程。作为最佳,应该始终为 ‘error’ 事件注册监听器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const EventEmitter = require ("events" ).EventEmitter ;var eventEmitter = new EventEmitter ();eventEmitter.on ("event" , function (a, b ) { console .log (a, b, this , this === eventEmitter); }); eventEmitter.emit ("event" , "a" , "b" ); eventEmitter.on ("event" , (a, b ) => { console .log (a, b, this , this === module .exports ); }); eventEmitter.emit ("event" , "a" , "b" );
事件模型设计(使用:发布订阅模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class EventEmitter { constructor (maxListeners ) { this .events = {}; this .maxListeners = maxListners || Infinity ; } on (event, listener ) { if (!this .events [event]) { this .events [event] = []; } if ( this .maxListener != Infinity && this .events [event].length > this .maxListeners ) { console .warn (`${event} has reached max listeners.` ); return this ; } this .events [event].push (listener); return this ; } off (event, listener ) { if (!listener) { this .events [event] = null ; } else { this .events [event] = this .events [event].filter ( (item ) => item !== listener ); } return this ; } once (event, listener ) { const func = (...args ) => { this .off (event, listener); listener.apply (this , args); }; this .on (event, func); } emit (event, ...args ) { const listeners = this .events [event]; if (!listeners) { console .log ("no listeners" ); return this ; } listeners.forEach ((cb ) => { cb.apply (this , args); }); return this ; } }
常用模块
fs:⽂件系统,能够读取写⼊当前安装系统环境中硬盘的数据
path:路径系统,能够处理路径之间的问题
crypto:加密相关模块,能够以标准的加密⽅式对我们的内容进⾏加解密
dns:处理 dns 相关内容,例如我们可以设置 dns 服务器等等
http: 设置⼀个 http 服务器,发送 http 请求,监听响应等等
readline: 读取 stdin 的⼀⾏内容,可以读取、增加、删除我们命令⾏中的内容
os:操作系统层⾯的⼀些 api,例如告诉你当前系统类型及⼀些参数
vm: ⼀个专⻔处理沙箱的虚拟机模块,底层主要来调⽤ v8 相关 api 进⾏代码解析
promisify:可以把异步函数回调形式改为 promise 形式。
fs
文件常识
权限位 mode:文件所有者/文件所属组/其他用户的 rwx,eg:默认 0o666,对应十进制 438
标志位 flag:文件的操作方式,r、w、s 同步、+增加相反操作、x 排他方式
文件描述符 fd:识别和追踪文件
完整性读写文件操作
fs.readFile(filename,[encoding],[callback(error,data)]
fs.writeFile(filename,data,[options],callback)
options:可选,为对象,{encoding, mode, flag}
默认编码为 utf8,模式为 0666(可读可写可操作),flag 为 ‘w’
fs.unlink(filename, callback)
指定位置读写文件操作(高级文件操作)
fs.open(path,flags,[mode],callback)
fs.read(fd, buffer, offset, length, position, callback);
fs.write(fd, buffer, offset, length, position, callback);
fs.close(fd,callback)
fd:文件描述符,需要先使用 open 打开,使用 fs.open 打开成功后返回的文件描述符;
buffer:一个 Buffer 对象,v8 引擎分配的一段内存,要将内容读取到的 Buffer;
offset:整数,向 Buffer 缓存区写入的初始位置,以字节为单位;
length:整数,读取文件的长度;
position:整数,读取文件初始位置;文件大小以字节为单位
callback:回调函数,有三个参数 err(错误),bytesRead(实际读取的字节数),buffer(被写入的缓存区对象),读取执行完成后执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const pathToFile = path.resolve (__dirname, "./text" );fs.readFile (pathTofile, "utf-8" , function (err, result ) { if (err) { console .log ("e" , e); return err; } console .log ("result" , result); }); fs.readFileSync (pathToFile, "utf-8" ); function read (path ) { return new Promise (function (resolve, reject ) { fs.readFile (path, { flag : "r" , encoding : "utf-8" }, function (err, data ) { if (err) { reject (err); } else { resolve (data); } }); }); } function promisify (func ) { return function (...args ) { return new Promise ((resolve, reject ) => { args.push (function (err, result ) { if (err) return reject (err); return resolve (result); }); return func.apply (func, args); }); }; } const creadFileAsync = promisify (fs.readFile );creadFileAsync (pathToFile, "utf-8" ) .then ((content ) => { console .log (content); }) .catch ((e ) => { console .log ("e" , e); });
大文件拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function copy (src, dest, size = 16 * 1024 , callback ) { fs.open (src, 'r' , (err, readFd ) => { fs.open (dest, 'w' , (err, writeFd ) => { let buf = Buffer .alloc (size); let readed = 0 ; let writed = 0 ; (function next ( ) { fs.read (readFd, buf, 0 , size, readed, (err, bytesRead ) => { readed += bytesRead; if (!bytesRead) fs.close (readFd, err => console .log ('关闭源文件' )); fs.write (writeFd, buf, 0 , bytesRead, writed, (err, bytesWritten ) => { if (!bytesWritten) { fs.fsync (writeFd, err => { fs.close (writeFd, err => return !err && callback ()); }); } writed += bytesWritten; next (); }); }); })(); }); }); }
目录(文件夹)操作
fs.mkdir: 创建目录
fs.rmdir: 删除目录
fs.access: 判断文件或目录是否有操作权限
fs.stat: 获取目录及文件信息
fs.readdir: 读取目录中内容
fs.unlink: 删除指定文件
path
path.join([path1][, path2][, …]):连接路径,主要用途在于,会正确使用当前系统的路径分隔符
path.resolve([from …], to):用于返回绝对路径,类似于多次 cd 处理
path.dirname(p):返回文件夹
path.extname(p):返回后缀名
path.basename(p):返回路径中的最后一部分,也就是名称
path.parse(): 解析路径,返回数组
path.format(): 序列化路径,将上面的数组重组成路径.
path.normalize(): 规范化路径
path.isAbsolute(): 判断传入路径是否是绝对路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const path = require ("path" );console .log ('join path : ' + path.join ('/test' , 'test1' , '2slashes/1slash' , 'tab' , '..' ));const mainPath = path.resolve ('main.js' )console .log ('resolve : ' + mainPath);console .log (path.resolve ()) console .log (path.resolve ('a' ,'b' )) console .log (path.resolve ('a' ,/b')) / / 返回`/ b` // resolve接收两部分参数,如果to的部分有路径(包含/),就返回'/xxx', // 如果不含路径,就将当前目录的绝对路径拼接上 // 路径中文件的后缀名:.js console.log('ext name : ' + path.extname(mainPath)); // 路径最后名字:main.js console.log('ext name : ' + path.basename(mainPath));
http 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 var http = require ("http" );var fs = require ("fs" );var url = require ("url" );http .createServer (function (request, response ) { var pathname = url.parse (request.url ).pathname ; console .log ("Request for " + pathname + " received." ); fs.readFile (pathname.substr (1 ), function (err, data ) { if (err) { console .log (err); response.writeHead (404 , { "Content-Type" : "text/html" }); } else { response.writeHead (200 , { "Content-Type" : "text/html" }); response.write (data.toString ()); } response.end (); }); }) .listen (8080 ); console .log ("Server running at http://127.0.0.1:8080/" );
Buffer Buffer 用于读取或操作二进制数据流,做为 Node.js API 的一部分使用时无需 require,用于操作网络协议、数据库、图片和文件 I/O 等一些需要大量二进制数据的场景。Buffer 在创建时大小已经被确定且是无法调整的. Buffer 是单独创建的,不占用 V8 的内存空间.
基本使用 Buffer 的创建 在此之前我们需要去简单的学习依一下 Buffer 的两个 API
Buffer.from(): 根据现有的数据结构去创建 buffer 数据
Buffer.alloc(): 指定 buffer 数据的长度来创建 buffer 数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const buf1 = Buffer .alloc (10 ); const buf2 = Buffer .alloc (10 , 1 ); const buf3 = Buffer .allocUnsafe (10 );const buf4 = Buffer .from ([1 , 2 , 3 ]); const buf5 = Buffer .from ("tést" ); const buf6 = Buffer .from ("tést" , "latin1" );
Buffer 的读写 关于 Buffer 的数据读取在官网上有很多方法:如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 buf.readBigInt64BE ([offset]); buf.readBigInt64LE ([offset]); buf.readBigUInt64BE ([offset]); buf.readBigUInt64LE ([offset]); buf.readDoubleBE ([offset]); buf.readDoubleLE ([offset]); buf.readFloatBE ([offset]); buf.readFloatLE ([offset]); buf.readInt8 ([offset]); buf.readInt16BE ([offset]); buf.readInt16LE ([offset]); buf.readInt32BE ([offset]); buf.readInt32LE ([offset]); buf.readIntBE (offset, byteLength); buf.readIntLE (offset, byteLength); buf.readUInt8 ([offset]); buf.readUInt16BE ([offset]); buf.readUInt16LE ([offset]); buf.readUInt32BE ([offset]); buf.readUInt32LE ([offset]); buf.readUIntBE (offset, byteLength); buf.readUIntLE (offset, byteLength);
这些方法名称有 BE 和 LE 的区别,这个大段和小段的意思,说白了就是方向的问题,而 Int8 是没有 BE 和 LE 的区别的,我们用代码来演示一下
1 2 3 4 5 6 7 8 buffer2.writeInt8 (12 , 1 ); console .log (buffer2); buffer2.writeInt16BE (512 , 2 ); console .log (buffer2); buffer2.writeInt16LE (512 , 2 ); console .log (buffer2);
所以你看到了,因为 int8 类型的数字只占一位,所以不存在方向不方向的问题,而 int16 类型的数字要占两位,比如 512 用 16 进制表示就是 [02 00],所以使用 BE 表示正方向,而 LE 表示反方向。具体使用什么样的表示方法要和被人协商议定
实例属性
buf.length:返回内存中分配给 buf 的字节数。 不一定反映 buf 中可用数据的字节量。
buf.toString:根据 encoding 指定的字符编码将 buf 解码成字符串
buf.fill:用指定的 value 填充 buf。 如果没有指定 offset 与 end,则填充整个 buf
buf.equals:如果 buf 与参数具有完全相同的字节,则返回 true,否则返回 false
buf.indexOf:是否包含参数,包含则返回所在下标,不包含返回-1
buf.slice: 截取 buffer
buf.copy: 拷贝 buffer 中的数据
常用静态属性 Buffer.byteLength 这个表示整个 buffer 实际所占的字节数,因为不同的语言可能有不同的问题,比如英文字母在表达的时候一个字母就是一个字节,而中文用三个字节来表达汉字
1 2 3 4 5 console .log (Buffer .byteLength ("test" )); console .log (Buffer .byteLength ("测试" )); console .log (Buffer .from ("test" ).length ); console .log (Buffer .from ("测试" ).length );
Buffer.isBuffer 1 2 3 console .log (Buffer .isBuffer ({})); console .log (Buffer .isBuffer (Buffer .from ("test" ))); console .log (Buffer .isBuffer (Buffer .from ([1 , 2 , 3 ])));
Buffer.concat 拼接 Buffer,注意的是里面不是传入多个 Buffer 对象,而是一个 Buffer 的数组
1 2 3 4 5 6 7 8 const buf1 = Buffer .from ("This " );const buf2 = Buffer .from ("is " );const buf3 = Buffer .from ("buffer " );const buf4 = Buffer .from ("test " );const buf5 = Buffer .from ("demo " );const bufConcat = Buffer .concat ([buf1, buf2, buf3, buf4, buf5]);console .log (bufConcat.toString ());
自定义 Buffer: split 拆分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ArrayBuffer .prototype .split = function (sep ) { let len = Buffer .from (sep).length ; let ret = []; let start = 0 ; let offset = 0 ; while ((offset = this .indexOf (sep, start) !== -1 )) { ret.push (this .slice (start, offset)); start = offset + len; } ret.push (this .slice (start)); return ret; }; let buf = "吃馒头, 吃面条, 吃所有" ;let bufArr = buf.split ("吃" );
乱码问题 乱码的出现因为是汉字是三个字节的,现在 11 个字节不是三个倍数,没有办法正确按照每 3 个字节解析汉字,我们的解决方法是:使用内置的 string_decoder(字符串解码器)来解析 Buffer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const { StringDecoder } = require ("string_decoder" );const decoder = new StringDecoder ("utf8" );const buf1 = Buffer .from ("我正在学习" );console .log (buf1); for (let i = 0 ; i < buf1.length ; i += 5 ) { const buf2 = Buffer .allocUnsafe (5 ); buf1.copy (buf2, 0 , i); console .log (decoder.write (buf2)); }
应用场景 I/O 操作 1 2 3 4 5 6 const fs = require ("fs" );const inputStream = fs.createReadStream ("input.txt" ); const outputStream = fs.createWriteStream ("output.txt" ); inputStream.pipe (outputStream);
在 Stream 中我们是不需要手动去创建自己的缓冲区,在 Node.js 的流中将会自动创建。
加密解密 在一些加解密算法中会遇到使用 Buffer,例如 crypto.createCipheriv 的第二个参数 key 为 String 或 Buffer 类型,如果是 Buffer 类型,就用到了本篇我们讲解的内容,以下做了一个简单的加密示例,重点使用了 Buffer.alloc()初始化一个实例(这个上面有介绍),之后使用了 fill 方法做了填充,这里重点在看下这个方法的使用。 buf.fill(value[, offset[, end]][, encoding])
value: 第一个参数为要填充的内容
offset: 偏移量,填充的起始位置
end: 结束填充 buf 的偏移量
encoding: 编码集
扩展 Buffer 和 Cache 的区别
缓冲(Buffer)是用于处理二进制流数据,将数据缓冲起来,它是临时性的,对于流式数据,会采用缓冲区将数据临时存储起来,等缓冲到一定的大小之后在存入硬盘中。视频播放器就是一个经典的例子,有时你会看到一个缓冲的图标,这意味着此时这一组缓冲区并未填满,当数据到达填满缓冲区并且被处理之后,此时缓冲图标消失,你可以看到一些图像数据。
缓存(Cache)我们可以看作是一个中间层,它可以是永久性的将热点数据进行缓存,使得访问速度更快,例如我们通过 Memory、Redis 等将数据从硬盘或其它第三方接口中请求过来进行缓存,目的就是将数据存于内存的缓存区中,这样对同一个资源进行访问,速度会更快,也是性能优化一个重要的点。
Buffer 与 String 比较 在 HTTP 传输中传输的是二进制数据,那么如果使用 Buffer 和 String 情况如下:
接口如果直接返回的字符串,这时候 HTTP 在传输之前会先将字符串转换为 Buffer 类型,以二进制数据传输,通过流(Stream)的方式一点点返回到客户端。
但是直接返回 Buffer 类型,则少了每次的转换操作,对于性能也是有提升的。
所以在一些 Web 应用中,对于静态数据可以预先转为 Buffer 进行传输,可以有效减少 CPU 的重复使用(重复的字符串转 Buffer 操作)
stream 介绍 在 node 当中 stream 是一种处理流数据的抽象接口,stream 模块提供了一系列实现流的 API,在 node 当中提供了很多关于流的对象,比如 http 服务器的请求和 process.stdout 标准输出都是流的实例,流是可读的,可写的,同时也可以是可读写的,所有的流都是 EventEmitter 的实例。
之所以用 stream ,是因为一次性读取、操作大文件,内存和网络是“吃不消”的,因此要让数据流动起来,一点一点的进行操作,这其实也符合算法中一个很重要的思想 —— 分而治之
流转和应用
data 事件: 用来监听 stream 数据的流入
end 事件:用来监听 stream 数据输入的完成
pipe 方法: 用来做数据流转
① stream 从哪里来-soucre stream 的常见来源方式有三种:
从控制台输入
http 请求中的 request
读取文件
这里先说一下从控制台输入这种方式,看一段 process.stdin 的代码:
1 2 3 4 process.stdin .on ("data" , function (chunk ) { console .log ("stream by stdin" , chunk); console .log ("stream by stdin" , chunk.toString ()); });
然后从控制台输入任何内容都会被 data 事件监听到,process.stdin 就是一个 stream 对象,data 是 stream 对象用来监听数据传入的一个自定义函数, (说明: stream 对象可以监听”data”,”end”,”opne”,”close”,”error”等事件。node.js 中监听自定义事件使用.on 方法,例如 process.stdin.on(‘data’,…), req.on(‘data’,…),通过这种方式,能很直观的监听到 stream 数据的传入和结束)② 连接水桶的管道-pipe 从水桶管道流转图中可以看到,在 source 和 dest 之间有一个连接的管道 pipe,它的基本语法是 source.pipe(dest) ,source 和 dest 就是通过 pipe 连接,让数据从 source 流向了 dest。③ stream 到哪里去-dest stream 的常见输出方式有三种:
输出控制台
http 请求中的 response
写入文件
我们上面借助控制台输入输出来讲解了 stream 的流转过程,可是在实际应用场景中,http 请求和文件操作当中使用 stream 的频率异常的频繁。原因是这样: http 请求和文件操作都属于 IO ,即 stream 主要的应用场景就是处理 IO ,这就又回到了 stream 的本质 —— 由于一次性 IO 操作过大,硬件开销太多,影响软件运行效率,因此将 IO 分批分段操作,让数据一点一点的流动起来,直到操作完成 。
http 中的 steam get 请求 1 2 3 4 5 6 7 8 9 10 11 12 const http = require ("http" );const server = http.createServer (function (req, res ) { const method = req.method ; if (method === "GET" ) { const fileName = path.resolve (__dirname, "data.txt" ); let stream = fs.createReadStream (fileName); stream.pipe (res); } }); server.listen (8000 );
从上面例子可以看出,对 response 使用 stream 特性能提高性能。因此,在 nodejs 中如果要返回的数据是经过 IO 操作得来的,例如上面例子中读取文件内容,可以直接使用 stream.pipe(res) ,毕竟 response 也是一个 stream 对象,可以作为参数传入 pipe 当中,而不要再用 res.end(data)了。 这种应用的实例应该比较多,主要有两种场景:
使用 node.js 作为服务代理,即客户端通过 node.js 服务作为跳板去请求其他服务,返回请求的内容
使用 node.js 做静态文件服务器,直接返回静态文件
post 请求 web server 接收 http 请求肯定是通过 request,而 request 接收数据的本质其实就是 stream。所以 看似是 request 接收数据,但是在服务端的角度来说,request 就是产生数据的 source,那么 soucre 类型的 stream 对象都能监听 data,end 事件。分别触发数据接收和数据接收完成的通知,所以代码可以如下进行改造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var http = require ("http" );var fs = require ("fs" );var path = require ("path" );var server = http.createServer (function (req, res ) { var method = req.method ; if (method === "POST" ) { req.on ("data" , function (chunk ) { console .log ("chunk" , chunk.toString ().length ); }); req.on ("end" , function ( ) { console .log ("end" ); res.end ("OK" ); }); } }); server.listen (8000 );
总结一下,request 和 response 一样,本身也是一个 stream 对象,可以用 stream 的特性,那肯定也能提高性能。两者的区别就在于,request 是 source 类型的、是 stream 的源头,而 response 是 dest 类型的、是 stream 的目的地。 这里再多举个例子,比如我们现在要将 node.js 接收到的 post 请求的数据写入文件,一般的小伙伴会这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var http = require ("http" );var fs = require ("fs" );var path = require ("path" );var server = http.createServer (function (req, res ) { var method = req.method ; if (method === "POST" ) { var dataStr = "" ; req.on ("data" , function (chunk ) { var chunkStr = chunk.toString (); dataStr += chunkStr; }); req.on ("end" , function ( ) { var fileName = path.resolve (__dirname, "post.txt" ); fs.writeFile (fileName, dataStr); res.end ("OK" ); }); } }); server.listen (8000 );
这种写法也是对的,但是还没有真正理解 stream,当我们学习了文件操作中的 stream,你可能就 hi 写出更简单的代码,比如说下面这种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var http = require ("http" );var fs = require ("fs" );var path = require ("path" );var server = http.createServer (function (req, res ) { var method = req.method ; if (method === "POST" ) { var fileName = path.resolve (__dirname, "post.txt" ); var writeStream = fs.createWriteStream (fileName); req.pipe (writeStream); req.on ("end" , function ( ) { res.end ("OK" ); }); } }); server.listen (8000 );
和 get 请求使用 stream 的场景类似,post 请求使用 stream 的场景,主要是用于将接收的数据直接进行 IO 操作,例如:
将接受的数据直接存储为文件
将接受的数据直接 post 给其他的 web server
fs 中的 stream 用 stream 读写文件其实前面章节都有多处用到,这里在统一整理一下:
可以使用 fs.createReadStream(fileName)来创建读取文件的 stream 对象
可以使用 fs.createWriteStream(fileName)来创建写入文件的 stream 对象
读取文件的 stream 对象,对应的就是 source,即数据的来源。写入文件的 steram 对象对应的就是 dest ,即数据的目的地。下面我们分别用普通的读写和使用 stream 实现一个文件拷贝的功能,并通过监控工具 memeye 来监控 node.js 内存占用的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const fs = require ("fs" );const path = require ("path" );const memeye = require ("memeye" );memeye ();function copy (num ) { const fileName1 = path.resolve (__dirname, "data.txt" ); fs.readFile (fileName1, function (err, data ) { if (err) { console .log ("读取出错" , err.message ); return ; } var dataStr = data.toString (); var fileName2 = path.resolve (__dirname, "data-bak.txt" ); fs.writeFile (fileName2, dataStr, function (err ) { if (err) { console .log ("写入出错" , err.message ); return ; } console .log (`拷贝第${num} 次成功` ); }); }); } setTimeout (() => { for (let i = 0 ; i < 100 ; i++) { copy (i); } }, 5000 );
启动文件,在浏览器打开 localhost:23333/,可以看到 heapUsed 在 13.26M 左右,heapTotal 在 30M 左右,rss 在 40M 左右
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const fs = require ("fs" );const path = require ("path" );const memeye = require ("memeye" );memeye ();function copy (num ) { var fileName1 = path.resolve (__dirname, "data.txt" ); var fileName2 = path.resolve (__dirname, "data-bak.txt" ); var readStream = fs.createReadStream (fileName1); var writeStream = fs.createWriteStream (fileName2); readStream.pipe (writeStream); readStream.on ("end" , function ( ) { console .log ("拷贝完成" ); }); } setTimeout (() => { for (let i = 0 ; i < 100 ; i++) { copy (i); } }, 5000 );
启动文件,在浏览器打开 localhost:23333/,可以看到 heapUsed 在 6.4M 左右,heapTotal 在 9.23M 左右,rss 在 30M 左右
总结:所有执行文件操作的场景,都应该尝试使用 stream ,例如文件的读写、拷贝、压缩、解压、格式转换等。除非是体积很小的文件,而且读写次数很少,性能上被忽略。如果是体积很大或者读写次数很多的情况下,建议使用 stream 来优化性能
逐行读取 readline 用 stream 操作文件,会来带很大的性能提升。但是原生的 stream 却对“行”无能为力,它只是把文件当做一个数据流、简单粗暴的流动。很多文件格式都是分行的,例如 csv 文件、日志文件,以及其他一些自定义的文件格式。
node.js 提供了非常简单的按行读取的 API —— readline,它本质上也是 stream,只不过是以“行”作为数据流动的单位。本节将结合一个分析日志文件的案例,讲解 readline 的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const fs = require ("fs" );const path = require ("path" );const readline = require ("readline" );var fileName = path.resolve (__dirname, "data.txt" );var readStream = fs.createReadStream (fileName);var readlineObj = readline.createInterface ({ input : readStream, }); readlineObj.on ("line" , (lineDate ) => { console .log (lineDate); console .log ("---- line ----" ); }); readlineObj.on ("close" , () => { console .log ("end" ); });
对 readline 最常见的使用应该是日志的逐行分析了,比如我们要从一个 10 万行的日志中去找一个 2018-10-23 14:00 这一分钟,对 user.html 的访问次数,我们就应该这样做
1 2 3 4 5 6 7 8 9 10 11 12 13 let num = 0 readlineObj.on ('line' ,function (lineData ){ if (lineData.indexOf ('2018-10-23 14:00' )>=0 && lineData.indexOf ('user.html' ) >= 0 ){ num++ } }) readline.on ('close' ,()=> { console .log ('num' ,num) })
借用这个简单的例子来演示 readline 的使用以及使用场景,其实日常工作中情况会更加复杂,不过再复杂的场景选择 readline 做逐行分析一定是不会错的。方向选对了很重要,选择大于努力.
stream 的种类 readable stream 可读流是对提供数据的来源的一种抽象。 可读流的例子包括:客户端的 HTTP 响应、服务器的 HTTP 请求、fs 的读取流、zilb 流、crypto 流、TCP socket、子进程 stdout 与 stderr、process.stdin,因为这些可读流都实现了 stream.Readable 类定义的接口
readable 事件: 当流中存在可读取数据时触发
data 事件: 当流中数据块传给消费者时触发
writeable stream 可写流是对数据要被写入的目的地的一种抽象. 可写流内部可写入的数据只支持 Buffer 和字符串. 可写流的例子包括:客户端的 HTTP 响应、服务器的 HTTP 请求、fs 的读取流、zilb 流、crypto 流、TCP socket、子进程 stdout 与 stderr、process.stdin,因为这些可写流都实现了 stream.Writable 类定义的接口
pipe 事件: 可读流调用 pipe()方法时触发
unpipe 事件: 可读流调用 unpipe()方法时触发
duplex stream 1 2 3 4 5 var fs = require ("fs" );var zlib = require ("zlib" );var readStream = fs.createReadStream ("./data.txt" );var writeStream = fs.createWriteStream ("./data-bak.txt" );readStream.pipe (zlib.createGzip ()).pipe (writeStream);
pipe 的严谨用法要遵循下面三个原则:
调用 pipe 的对象必须是 readable stream 或者 duplex stream 这样的具有读取数据的功能的流对象
pipe 中的参数对象必须是 writeable stream 或者 duplex stream 这样的具有写入数据的功能的流对象
pipe 支持链式调用
转换流(Transform)是一种 Duplex 流,但它的输出与输入是相关联的,而且一般只有触发了写入操作时才会进入_transform 方法中。 与 Duplex 流一样, Transform 流也同时实现了 Readable 和 Writable 接口。
写入流程
第一次调用 write 方法时是将数据直接写入到文件中
第二次开始 write 方法就是将数据写入至缓存中
生产速度 和消费速度是不一样的,一般情况下生产速度要比消费速度快很多
当 flag 为 false 之后并不意味着当前次的数据不能被写入了.但是我们应该告之数据的生产者,当前的消费速度已经跟不上生产速度了,所以这个时候,一般我们会将可读流的模块修改为暂停模式。
当数据生产者暂停之后,消费者会慢慢的消化它内部缓存中的数据,直到可以再次被执行写入操作
当缓冲区可以继续写入事件如何让生产者知道? 使用 drain 事件.
背压机制 因为流的读取操作中,读的速度大于写的速度,为了控制,防止内存溢出,GC 频繁调用.
stream 有什么弊端
用 rs.pipe(ws)
的方式来写文件并不是把 rs 的内容 append 到 ws 后面,而是直接用 rs 的内容覆盖 ws 原有的内容
已结束/关闭的流不能重复使用,必须重新创建数据流
pipe 方法返回的是目标数据流,如 a.pipe(b)
返回的是 b,因此监听事件的时候请注意你监听的对象是否正确
如果你要监听多个数据流,同时你又使用了 pipe 方法来串联数据流的话,你就要写成:
1 2 3 4 5 6 7 8 9 10 11 12 data .on ("end" , function ( ) { console .log ("data end" ); }) .pipe (a) .on ("end" , function ( ) { console .log ("a end" ); }) .pipe (b) .on ("end" , function ( ) { console .log ("b end" ); });
promisify 1 2 3 4 5 import { readFile } from "fs" ;import { promisify } from "uitl" ;const read = promisify (readFile);
链表 可理解为一种存储数据的结构. 链表是一组节点组成的集合,每个节点都使用一个对象的引用来指向它的后一个节点。指向另一节点的引用讲做链。
链表结构图 1 data1|next => data2|next => data3|next => Null
data 中保存着数据,next 保存着下一个链表的引用。上图中,我们说 data2 跟在 data1 后面,而不是说 data2 是链表中的第二个元素。上图,值得注意的是,我们将链表的尾元素指向了 null 节点,表示链接结束的位置。
有头节点的链表 由于链表的起始点的确定比较麻烦,因此很多链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点 ,表示链表的头部。进过改造,链表就成了如下的样子:
1 Header |next => data1|next => data2|next => data3|next => Null
插入节点 向链表中插入一个节点 的效率很高,需要修改它前面的节点(前驱),使其指向新加入的节点,而将新节点指向原来前驱节点指向的节点即可。下面我将用图片演示如何在 data2 节点 后面插入 data4 节点。
1 2 3 4 Header |next => data1|next => data2|next => data3|next => Null | ∧ ∨ | data4 | next
删除节点 同样,从链表中删除一个节点,也很简单。只需将待删节点的前驱节点指向待删节点的,同时将待删节点指向 null,那么节点就删除成功了。下面我们用图片演示如何从链表中删除 data4 节点
1 2 3 Header |next => data1|next => data2|next => data3|next data4|next Null | ∧ ∨ ----------------- |
单向链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 class Node { constructor (element, next ) { this .element = element this .next = next } } class LinkedList { constructor (head, size ) { this .head = null this .size = 0 } _getNode (index ){ if (index<0 || index >= this .size ) { throw new Error ('越界' ) } let currentNode = this .head for (let i=0 ; i < index; i++) { currentNode = currentNode.next } return currentNode } add (index, element ) { if (argument.length ===1 ){ element = index index = this .size } if (index < 0 || index > this .size ) { throw new Error ('越界 } // 判断位置 if(index === 0) { let head = this.head // 保存原有head的指向 this.head = new Node(element, head) // 现在的head指向newNode }else { // 如果不在第一个位置,获取上一个节点 let prevNode = this._getNode(index-1) // 上一Node 的next指向新节点 prevNode.next = new Node(element, prevNode.next) } // add后注意size变化 this.size++ } // 删 remove(index){ let rmNode = null if(index === 0) { //如果删的是第一个 rmNode = this.head if(!rmNode) { return undefined } this.head = head.next } else { let prevNode = _getNode(index -1) rmNode = prevNode.next prevNode.next = rmNode.next } this.size-- return rmNode } // 改 set(index, element) { let node = _getNode(index) node.element = element } // 查 get(index) { return this._getNode(index) } // 清空 clear(){ this.head = null this.size = 0 } } const l1 = new LinkedList() li.add(' node1')
单向链表队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Queue { constructor ( ) { this .linkedList = new LinkedList (); } enqueue (data ) { this .linkedList .add (data); } dequeue ( ) { return this .linkedList .remove (0 ); } } const q = new Queue ();