nodejs学习笔记

143 阅读8分钟

基础知识点

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()

指示引擎在当前操作结束(在下一个事件循环滴答开始之前)时调用此函数;每当事件循环进行一次完整的行程时,我们都将其称为一个滴答。

文件

文件基础知识

文件路径

  1. dirname: 获取文件的父文件夹
  2. basename: 获取文件名部分
  3. extname: 获取文件的扩展名
  4. join: 连接路径点多个片段
  5. resolve: 获得文件的绝对路径
  6. 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+ 打开文件用于读写,将流定位到文件末尾。如果文件不存在则创建。

读取文件

  1. read
  2. readFile()
  3. 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');
})

自定义可读流

image.png 如果我们想自己以某种特定的方式生产数据,交给程序消费。那么就需要自定义可读流;简单两步即可

继承 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频繁调用、其它进程变慢

image.png

image.png

背压机制代码实现

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() 执行时候,会判断写入这个值后缓存区是否满了

文件可写流执行步骤

  1. 第一次调用 write 方法时是将数据直接写入到文件中
  2. 第二次开始 write 方法就是将数据写入缓存中
  3. 生产速度和消费速度是不一样的,一般情况下生产速度要比消费速度快很多
  4. 当 flag 为 false 之后,表示消费速度跟不上生产速度了,这个会后一般会将可读流的模块修改为暂停模式
  5. 当数据生产暂停之后,消费者会慢慢消化缓存中的数据,直到可以再次被执行写入操作
  6. 当缓存区可以继续写入数据时该如何让生产者知道。

模拟 createWriteStream

链表结构

image.png

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 image.png

创建一个 launch.json 文件,选择环境 node.js image.png

注释掉这一行 image.png 这个地址选择你要运行的文件就可以了

保存,关闭后也会提示保存;点击运行 image.png