基础知识点
node 可以做什么
- 轻量级、高性能的 web 服务
- 前后端 javascript 同构开发
- 便携高效的前端工程化
node 执行队列
- timer: 执行 setTimeout 与 setInterval 回调
- pending callbacks: 执行系统操作的回调,例如 tcp udp
- idel,prepare: 只在系统内部进行使用
- poll: 执行与 I/O 相关的回调
- check: 执行 setImmediate 中的回调
- close callbacks: 执行 close 事件的回调
node 中执行会按照上面的顺序执行,开发者一般只需要用到 timer、poll、check;
Nodejs 完整事件环
- 执行同步代码,将不同的任务添加至相应的队列
- 所有同步代码执行后会去执行满足条件微任务
- 所有微任务代码执行后会执行 timer 队列中满足的宏任务
- timer 中的所有宏任务执行完后就会依次切换队列
- 注意:在完成嘟列切换之前会先清空微任务队列
这里有个有趣的例子
setTimeout(() => console.log('setTimeout'))
setImmediate(() => console.log('setImmediate'))
这个没有固定的打印顺序。原因就是不设置 setTimeout 的时间时,默认是1,不是0; 电脑执行有时快有时慢;有时执行到 timer 时候时间小于1,那么 setTimeout 的回调函数 还没进来,这时候就是先打印 setImmediate;如果时间大于1,那么 setTimeout 的回调 已经放入队列了,就会先执行 setTimeout 了。
process.nextTick()
指示引擎在当前操作结束(在下一个事件循环滴答开始之前)时调用此函数;每当事件循环进行一次完整的行程时,我们都将其称为一个滴答。
文件
文件基础知识
文件路径
- dirname: 获取文件的父文件夹
- basename: 获取文件名部分
- extname: 获取文件的扩展名
- join: 连接路径点多个片段
- resolve: 获得文件的绝对路径
- normalize: 尝试计算文件的实际路径
const path = require('path')
const url = './user/test/index.js'
console.log(path.dirname(url)); // ./user/test
console.log(path.basename(url)); // index.js
console.log(path.extname(url)); // .js
console.log(path.basename(url, path.extname(url))); // index
console.log(path.join('./user', 'temp', 'index')); // user/temp/index
console.log(path.resolve(url)); // /Users/zhangqi/Desktop/study/node-demo/user/test/index.js
console.log(path.normalize('/users/joe/..//test.txt')); // /users/test.txt
解析和规范化都不会检查路径是否存在。 其只是根据获得的信息来计算路径。
文件属性
const fs = require('fs')
fs.stat('./package.json', (err, status) => {
if (err) {
console.log(err);
return;
}
console.log(status);
// 常用方法
console.log(status.isFile());
console.log(status.isDirectory());
console.log(status.isSymbolicLink());
console.log(status.size);
})
try{
const status = fs.statSync('./package.json');
console.log(status);
} catch (e) {
console.log(e)
}
文件夹的操作
创建文件夹
const fs = require('fs')
const folderName = './users'
try {
if (!fs.existsSync(folderName)) {
fs.mkdirSync(folderName);
}
} catch (e) {
console.log(e);
}
不能直接创建多层,创建文件夹必须基于已有的父文件夹;
删除文件夹
使用 fs.rmdir() 或 fs.rmdirSync() 可以删除文件夹。
删除包含内容的文件夹可能会更复杂。最好安装 fs-extra 模块,它是 fs 模块的直接替代品,在其之上提供了更多的功能。
npm install fs-extra
const fs = require('fs-extra')
fs.remove('./users', err => {
if (err) {
console.log(err);
return;
}
console.log('remove success');
})
fs.remove()方法返回一个promise,还可以这样使用
const fs = require('fs-extra')
fs.remove('./users')
.then(() => {
console.log('remove success');
})
.catch(e => {
console.log(e);
})
并像这样使用它:
修改文件夹名称
使用 fs.rename() 或 fs.renameSync() 可以重命名文件夹。 第一个参数是当前的路径,第二个参数是新的路径:
const fs = require('fs-extra')
fs.rename('./user', './age', err => {
console.log(err ? err : 'rename success');
})
同步方式
try {
fs.renameSync('./user', './age');
console.log('rename success');
} catch (e) {
console.log(e);
}
修改文件名称和修改文件夹名称用相同的方式
查询文件夹下内容
使用 fs.readdir() 或 fs.readdirSync() 可以读取目录的内容。
const fs = require('fs-extra')
fs.readdir('./users', (err, data) => {
if (err) {
console.log(err);
return;
}
console.log(data);// [ 'components', 'index.html', 'index.js' ]
})
同步方式
try {
const result = fs.readdirSync('./users');
console.log(result); // [ 'components', 'index.html', 'index.js' ]
} catch (e) {
console.log(e);
}
文件的操作
打开文件
const fs = require('fs')
fs.open('./package.json', 'r', (err, fd) => {
if(err){
console.log(err)
return;
}
console.log(fd)
})
try{
const fd = fs.openSync('./package.json', 'r');
console.log(fd);
}catch (e) {
console.log(e);
}
r 是打开文件的标志,常用的有如下
r打开文件用于读r+打开文件用于读写w打开文件用于写,将流定位在文件开头。如果文件不存在则创建。w+打开文件用于读写,将流定位在文件开头。如果文件不存在则创建a打开文件用于写,将流定位到文件末尾。如果文件不存在则创建。a+打开文件用于读写,将流定位到文件末尾。如果文件不存在则创建。
读取文件
- read
- readFile()
- readFileSync()
fs.read
fs.read(fd, buffer, offset, length, position, callback)
- offset 要写入数据的 buffer 中的位置
- length 读取的字节数
- position 指定从文件中开始读取的位置,null 或 -1,则将从当前文件位置读取数据,并更新文件位置。 如果
position是整数,则文件位置将保持不变。 - callback
- err
- bytesRead
- buffer
const fs = require('fs')
fs.open('test.txt', (err, fd) => {
let buffer = Buffer.alloc(1024);
fs.read(fd, buffer, 2, 6, 0, (err, bytes, buf) => {
console.log(bytes); // 这个值为 0 表示读取完毕
console.log(buf.toString());
console.log(buffer);
console.log(buffer.toString());
})
})
readFile
fs.readFile(url, [code], callback)
const fs = require('fs')
fs.readFile('./package.json', 'utf8', (err, data) => {
if (err) {
console.log(data);
return;
}
console.log(data);
})
try {
const data = fs.readFileSync('./package.json', 'utf8');
console.log(data);
} catch (e) {
console.log(e);
}
readFile 会在返回数据之前将文件的全部内容读取到内存中。这意味着大文件会对内存的消耗和程序执行的速度产生重大的影响。可以使用 read 分段读取文件,更好的是使用流来读取文件的内容。
写入文件
- write
- writeFile
- appendFile
write
fs.write(fd, buffer[, offset[, length[, position]]], callback)
const fs = require('fs')
fs.open('test.txt', 'w', (err, fd) => {
let buffer = Buffer.from('今天是个好日子');
fs.write(fd, buffer, 3, 6, 0, (err, byteWritten, buf) => {
console.log(byteWritten); // 这个值为 0 表示写入完毕
console.log(buf);
console.log(buf.toString());
})
})
writeFile
fs.wirteFile(url, content, [options], callback)
- options
- encoding: 默认 'utf8'
- mode: 默认 0o666
- flag: 默认 'w'
- signal: 允许终止正在运行的写入文件
- callback
- err
const fs = require('fs')
const content = 'hello';
fs.writeFile('./file.js', content, {flag: 'a'}, err => {
if (err) {
console.log(err);
return;
}
console.log('write success');
})
try {
fs.writeFileSync('./file.js', content);
console.log('write success');
} catch (e) {
console.log(e);
}
appendFile
fs.appendFile(url, content, callback)
const fs = require('fs')
const content = 'hello';
fs.appendFile('./file.js', content, err => {
if (err) {
console.log(err);
return;
}
console.log('append success');
})
try {
fs.appendFileSync('./file.js', content);
console.log('append success');
} catch (e) {
console.log(e);
}
writeFile 和 appendFile 都是一次性的将内容写入文件,大文件效果不好。除了可以用 write 分段写入,更推荐使用流来写入;
流
介绍
什么是流
流是为 Node.js 应用程序提供动力的基本概念之一。
它们是一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换。
在传统读取文件时,会将文件从头到尾读入内存,然后进行处理。使用流,则可以逐个片段地读取并处理(而无需全部保存在内存中)。
Node.js 的 stream 模块 提供了构建所有流 API 的基础。 所有的流都是 EventEmitter 的实例。
使用流的两个优点
- 内存效率: 无需加载大量的数据到内存中即可进行处理。
- 时间效率: 当获得数据之后即可立即开始处理数据,这样所需的时间更少,而不必等到整个数据有效负载可用才开始。
流的分类
- Readable: 可读流,实现数据的读取
- Writeable: 可写流,实现数据的写操作
- Duplex: 双工流,即可读又可写
- Tranform: 转换流,可读可写,还能实现数据转换 上面这四个是抽象,其它的操作都是继承于以上
简单的拷贝操作
const fs = require('fs')
const read = fs.createReadStream('./package.json');
const write = fs.createWriteStream('./test.txt')
read.pipe(write);
可读流
可读流是生产数据用来供程序消费的流。我们常见的数据生产方式有读取磁盘文件、读取网络请求内容等。
可读流常用事件
- readable 事件:当流中存在可读数据时触发
- data 事件:当流中数据块传给消费者后触发
创建可读流、读取文件
fs.createReadStream(path[, options])
const fs = require('fs');
let rs = fs.createReadStream('test.txt', {
flags: 'r',
encoding: null,
fd: null, // 默认 null
mode: 438, // 默认 0o666
autoClose: true,
start: 0, // 开始读取位置
// end: 3, // 结束位置
highWaterMark: 2, // 每次读取到缓冲区的长度,默认 64*1024
fs: null // 默认 null
})
rs.on('data', chunk => {
console.log(chunk.toString());
// 下面是暂停和重新执行
rs.pause();
setTimeout(() => {
rs.resume();
}, 200)
})
rs.on('readable', () => {
let data = null;
console.log('readable 读取到缓冲区次数')
while ((data = rs.read(2)) !== null){
console.log(data.toString());
console.log(rs._readableState.length); // 缓冲区中有多少
console.log('从缓冲区读取次数')
}
})
highWaterMark是每次读取到缓冲区中的长度, read(n)中的 n 是每次从缓冲区读取的长度;
文件可读流事件与应用
const fs = require('fs');
const rs = fs.createReadStream('test1.txt', {
flags: 'r',
encoding: null,
mode: 438,
start: 0,
autoClose: true, // 默认true
highWaterMark: 3
});
// fs.createReadStream 执行时候触发
rs.on('open', fd => {
console.log(`${fd} 打开了`);
})
// 上面设置了 autoClose: true, 文件在读取完后会自动关闭
rs.on('close', () => {
console.log('文件关闭了');
})
rs.on('data', chunk => {
console.log(chunk.toString());
})
rs.on('readable', () => {
let data = null;
while ((data = rs.read()) !== null){
console.log(data.toString())
}
})
rs.on('end', () => {
console.log('当数据被清空之后')
})
rs.on('error', err => {
console.log('错误是:' + err);
})
在实际使用中,一般是将读取到的数据存入Buffer,然后在end事件中使用
const bufferArr = [];
rs.on('data', chunk => {
bufferArr.push(chunk);
})
rs.on('end', () => {
console.log(Buffer.concat(bufferArr).toString());
})
模拟 createReadStream
模拟是为了更好的了解和使用 createReadStream
const fs = require('fs')
const EventEmitter = require('events');
class MyFileReadStream extends EventEmitter {
constructor(path, options = {}){
super();
this.path = path;
this.flags = options.flags || 'r';
this.mode = options.mode || 438;
this.autoClose = options.autoClose || true;
this.start = options.start || 0;
this.end = options.end;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.readOffSet = 0;
this.open();
// 监听外部绑定的事件 rs.on(xx)
this.on('newListener', type => {
if(type === 'data'){
this.read()
}
})
}
open(){
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if(err){
this.emit('error', err);
return;
}
this.fd = fd;
this.emit('open', fd);
})
}
read(){
console.log(typeof this.fd);
if(typeof this.fd !== 'number'){
return this.once('open', this.read);
}
let buf = Buffer.alloc(this.highWaterMark);
fs.read(this.fd, buf, 0, this.highWaterMark, this.readOffSet, (err, readBytes) => {
if(readBytes){
console.log(readBytes);
this.readOffSet += readBytes;
this.emit('data', buf);
this.read();
}else{
this.emit('end');
this.close();
}
})
}
close(){
fs.close(this.fd, () => {
this.emit('close');
})
}
}
const rs = new MyFileReadStream('test.txt',{
highWaterMark: 3
});
rs.on('open', fd => {
console.log(`${fd} 打开了`);
})
rs.on('error', err => {
console.log(err);
})
rs.on('data', chunk => {
console.log(chunk.toString());
})
rs.on('end', () => {
console.log('end');
})
rs.on('close', () => {
console.log('close');
})
自定义可读流
如果我们想自己以某种特定的方式生产数据,交给程序消费。那么就需要自定义可读流;简单两步即可
继承 sream 模块的 Readable 类
重写 _read 方法,调用 this.push 将生产的数据放入待读取队列
可读流代码实现
const { Readable } = require('stream');
class MyReadable extends Readable{
constructor(props) {
super(props);
this.source = props;
}
_read(){
this.push(this.source.shift() || null);
}
}
const myRead = new MyReadable(['how', 'are', 'you']);
myRead.on('readable', () => {
let data = null;
while ((data = myRead.read()) !== null){
console.log(data.toString());
}
})
// howare
// you
myRead.on('data', (chunk) => {
console.log(chunk.toString());
})
// how
// are
// you
可写流
可写流事件
- pipe: 可读流调用 pipe() 方法时触发
- unpipe: 可读流调用 unpipe() 方法时触发
文件写入操作
const fs = require('fs')
const ws = fs.createWriteStream('test.txt',{
flags: 'w',
mode: 438,
fd: null,
encoding: 'utf-8',
start: 0,
highWaterMark: 3 // 默认16kb
});
// 字符串或者 buffer
ws.write('hello world', () => {
console.log('数据写完了')
})
// 执行这个意味着数据写入操作完成,后面不可以在直接写入
// 这个可以不用传参
ws.end('你是谁', () => {
console.log('数据写完了3')
})
ws.on('open', fd => {
console.log(`${fd} 打开了`)
})
// 文件全部写入完成后触发 也就是 ws.end() 之后
ws.on('close', () => {
console.log('文件关闭了')
})
ws.on('error', err => {
console.log(err)
})
控制写入速度
pipe() 管道方式中已经有处理过
/*
1.分批写入
2.控制速度
*/
const fs = require('fs');
const ws = fs.createWriteStream('test.txt',{
highWaterMark: 3 // 一个中文为3
})
let source = '拉钩教育'.split('');
let flag = true;
let num = 0;
function executeWrite(){
flag = true;
while(num < 4 && flag){
flag = ws.write(source[num]);
num++;
}
}
executeWrite();
// 监听到缓存区清空了
ws.on('drain', () => {
console.log('drain 执行了');
executeWrite();
})
背压机制
一般消费速度低于生产速度,有可能造成内存溢出、GC频繁调用、其它进程变慢
背压机制代码实现
const fs = require('fs')
const rs = fs.createReadStream('test.txt',{
highWaterMark: 6
});
const ws = fs.createWriteStream('test1.txt',{
highWaterMark: 1
});
let flag = true;
rs.on('data', chunk => {
flag = ws.write(chunk, () => {
console.log('写完了');
})
// 当缓存满了,暂停读取
if(!flag){
rs.pause();
}
})
// 当缓存清空时候,重新读取
ws.on('drain', () => {
rs.resume();
})
pipe()已经实现了以上,在实际使用中直接使用就可以了
rs.pipe(ws);
write 执行流程
const fs = require('fs')
const ws = fs.createWriteStream('test.txt',{
flags: 'w',
mode: 438,
fd: null,
encoding: 'utf-8',
start: 0,
highWaterMark: 1 // 默认16kb
});
let flag = ws.write('1');
console.log(flag);
flag = ws.write('2');
console.log(flag);
flag = ws.write('3');
console.log(flag);
flag = ws.write('4');
console.log(flag);
// 这个 flag 为 false,并不表示不可以写入
// 表示的是缓存区满了,也就是 highWaterMark
// ws.write() 执行时候,会判断写入这个值后缓存区是否满了
文件可写流执行步骤
- 第一次调用 write 方法时是将数据直接写入到文件中
- 第二次开始 write 方法就是将数据写入缓存中
- 生产速度和消费速度是不一样的,一般情况下生产速度要比消费速度快很多
- 当 flag 为 false 之后,表示消费速度跟不上生产速度了,这个会后一般会将可读流的模块修改为暂停模式
- 当数据生产暂停之后,消费者会慢慢消化缓存中的数据,直到可以再次被执行写入操作
- 当缓存区可以继续写入数据时该如何让生产者知道。
模拟 createWriteStream
链表结构
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(arguments.length === 1){
element = index;
index = this.size;
}
if(index < 0 || index > this.size){
throw new Error('cross the border');
}
if(index === 0){
let head = this.head;
this.head = new Node(element, head);
}else{
let preNode = this._getNode(index - 1);
preNode.next = new Node(element, preNode.next);
}
this.size++;
}
remove(index){
let rmNode = null;
if(index === 0){
rmNode = this.head;
if(!rmNode){
return undefined;
}
this.head = rmNode.next;
}else{
let preNode = this._getNode(index - 1);
rmNode = preNode.next
preNode.next = rmNode.next;
}
this.size--;
return rmNode;
}
set(index, element){
let node = this._getNode(index);
node.element = element;
}
get(index){
return this._getNode(index);
}
clear(){
this.head = null;
this.size = 0;
}
}
// 队列
class Queue {
constructor(){
this.linkedList = new LinkedList();
}
// 进队列
enQueue(data){
this.linkedList.add(data);
}
// 出队列
deQueue(){
return this.linkedList.remove(0);
}
}
const q = new Queue();
q.enQueue('que1');
q.enQueue('que1');
let a = q.deQueue();
模拟 createWriteStream 代码实现
const fs = require('fs');
const EventEmitter = require('events');
const Queue = require('./Queue.js');
class MyWriteStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.flags = options.flags || 'w';
this.mode = options.mode || 438;
this.autoClose = options.autoClose || true;
this.start = options.start || 0;
this.encoding = options.encoding || 'utf8';
this.highWaterMark = options.highWaterMark || 16 * 1024;
this.open();
this.writeoffset = this.start;
this.writing = false;
this.writeLen = 0;
this.needDrain = false;
this.cache = new Queue();
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err);
return;
}
this.fd = fd;
this.emit('open', fd);
})
}
write(chunk, encoding, cb) {
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
this.writeLen += chunk.length;
let flag = this.writeLen < this.highWaterMark;
this.needDrain = !flag;
if (this.writing) {
// 当前正在写入,内容应该排队
this.cache.enQueue({ chunk, encoding, cb });
} else {
this.writing = true;
// 当前不是正在写入那么就执行写入
this._write(chunk, encoding, () => {
cb();
// 清空排队的内容
this._clearBuffer();
});
}
return flag;
}
_write(chunk, encoding, cb) {
if (typeof this.fd !== 'number') {
return this.once('open', () => {
console.log('once open');
return this._write(chunk, encoding, cb);
})
}
fs.write(this.fd, chunk, this.start, chunk.length, this.writeoffset, (err, written) => {
this.writeoffset += written;
this.writeLen -= written;
cb && cb();
})
}
_clearBuffer() {
let data = this.cache.deQueue();
if (data) {
this._write(data.element.chunk, data.element.encoding, () => {
data.element.cb && data.element.cb();
this._clearBuffer();
})
} else {
if (this.needDrain) {
this.needDrain = false;
this.emit('drain');
}
}
}
}
const ws = new MyWriteStream('./test.txt');
ws.on('open', fd => {
console.log(`${fd} 打开了`);
})
ws.write('hello world', 'uft8', () => {
console.log('写入成功1');
})
ws.write('hello world2', 'uft8', () => {
console.log('写入成功2');
})
模拟pipe
const EventEmitter = require('events');
class MyReadStream extends EventEmitter{
pipe(ws){
this.on('data', data => {
let flag = ws.write(data);
if(!flag){
this.pause();
}
})
ws.on('drain', () => {
this.resume();
})
}
}
自定义可写流
const { Writable } = require('stream');
class MyWriteable extends Writable{
constructor(props) {
super(props);
}
_write(chunk, en, done){
process.stdout.write(chunk.toString() + '----');
process.nextTick(done);
}
}
const myWriteable = new MyWriteable();
myWriteable.write('拉钩教育', 'utf-8', () => {
console.log('写入成功');
})
双工流
const { Duplex } = require('stream');
let source = ['hello', 'world'];
class MyDuplex extends Duplex{
constructor(props) {
super(props);
this.source = props;
}
_read(){
this.push(this.source.shift() || null);
}
_write(chunk, enc, next){
if(Buffer.isBuffer(chunk)){
chunk = chunk.toString();
}
process.stdout.write(chunk + '----')
process.nextTick(next);
}
}
let myDuplex = new MyDuplex(source);
myDuplex.on('data', (chunk) => {
console.log(chunk.toString());
})
myDuplex.write('测试数据', 'utf-8', () => {
console.log('双工流测试可写操作');
})
转换流
const { Transform } = require('stream');
class MyTransform extends Transform {
constructor(props) {
super(props);
}
_transform(chunk, enc, callback){
this.push(chunk.toString().toUpperCase());
callback();
}
}
let a = new MyTransform();
a.write('a')
a.write('b')
a.end('c')
a.on('data', chunk => {
console.log(chunk.toString());
})
// a.pipe(process.stdout)
node调试-进入node源码
点击 Run and Debug
创建一个 launch.json 文件,选择环境 node.js
注释掉这一行
这个地址选择你要运行的文件就可以了
保存,关闭后也会提示保存;点击运行