什么是流?
根据Node.js官网解释,流(stream)是一种在 Node.js 中处理流式数据的抽象接口。
1、抽象接口就是将数据的传输隐喻为水管之间的连接,水管里面的水就是数据
2、是有方向、可连接的
3、数据格式通常是Buffer
- 流共分四种
Writable(可写流)
Readable(可读流)
Duplex(可读写流,又称双工流)
Transform(属于Duplex的一种)
- Node.js的诸多方法如http请求都是流的实例
- 而流是EventEmitter的实例(容易被忽视)
可写流
现在我们来实现可写流
let fs = require('fs');
let EventEmiter = require('events');
// 根据可写流的输出
// 当highWaterMark小于我们写的内容长度 先触发drain 后触发回调函数
// 当highWaterMark大于我们写的内容长度 先触发回调函数一次 再触发drain 再触发剩余回调函数
// 虽然很奇怪但是没办法 = =
// 所以我定了个fisrt变量确定highWaterMark和内容长度的关系
let first = undefined;
let firstFlag = true;
/**
* --------------------------------------------------
* Node.js源代码中
* WriteStream继承(call)Writable
* Writable继承(call)Stream
* Stream继承(call)EE(EventEmiter)
* 是一个链式关系
* 所以我们直接继承EventEmiter
* 并且将Writable中的write等方法写在WriteStream中
*
* @param {string} path 地址
* @param {object} options 配置
* @extends EventEmiter
* @this WriteStream
* --------------------------------------------------
*/
class WriteStream extends EventEmiter {
constructor(path, options) {
super();
// 设置options配置
options = copyObject(getOptions(options, {}));
// path必须是string类型
if (typeof path !== 'string') throw Error('path must be string');
this.path = path;
// 从第几个开始写
this.start = options.start || 0;
// 文件标识符
this.fd = options.fd === undefined ?null:options.fd;
// 默认write
this.flags = options.flags === undefined ? 'w' : options.flags;
// 默认0o666
this.mode = options.mode === undefined ? 0o666 : options.mode;
// 默认自动关闭
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
// 编码默认utf8
this.encoding = options.encoding || 'utf8';
// 高水位线 默认64k
this.highWaterMark = options.highWaterMark || 16 * 1024;
// 触发drain事件的条件
this.needDrain = false;
// 是否正在写入,初始为未写
this.iswriting = false;
// 缓存
this.cache = [];
// 缓存长度
this.cacheLength = 0;
// 偏移量,第一次后每次开始写的位置
this.pos = this.start;
// write回调函数 储存
this.cbCache = [];
// 一创建,就执行open
this.open();
}
// 打开文件
open() {
/**
* 打开文件
* @param {string|Buffer|URL} path 地址
* @param {string|number} flags 默认w(会先清空文件内容)
* @param {integer} mode 读写权限
* --------------------------------------------------
*/
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
this.emit('error');
this.destory();
return;
}
this.fd = fd;
this.emit('open');
})
}
// 关闭文件
destory() {
// 存在文件标识符fd 则关闭
if (typeof this.fd === 'number') {
fs.close(this.fd, () => {
// 触发close
this.emit('close');
});
return;
}
// 触发close
this.emit('close');
}
/**
* write 外部调用的 write方法
* @param {string|Buffer} chunk 写入内容
* @param {string} encoding 编码
* @param {function} callback 回调函数
* --------------------------------------------------
*/
write(chunk, encoding = this.encoding, callback) {
// 不是Buffer格式 就转成 Buffer格式
if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
// 缓存长度增加 增加数量为 chunk长度
this.cacheLength += chunk.length;
// needDrain代表是否需要触发drain
// 如果缓存长度 大于 高水位线,代表数据量超过最高通过量,needDrain为true
// 如果缓存长度 小于 高水位线,代表数据量低于最高通过量,needDrain为false
this.needDrain = this.highWaterMark <= this.cacheLength;
if(firstFlag){
first = this.needDrain;
firstFlag = false;
};console.log(first);
// 当正在写时,缓存增加,写完在写
if (this.iswriting) {
this.cache.push({
chunk,
encoding,
callback
})
}else{
// 实际第一次开始写的位置,使iswriting为true
this.iswriting = true;
// 内部write方法,传入chunk、encoding、
this._write(chunk,encoding,()=>this.clearBuffer(),callback);
}
// write必须有为布尔值的返回值
return !this.needDrain;
}
/**
* 内部的_write方法
* @param {string|Buffer} chunk 写入内容
* @param {string} encoding 编码
* @param {function} clear clearBuffer函数
* @param {function} callback write的回调函数
*/
_write(chunk,encoding,clear,callback){
//如果write时 还没获取到fd
if(typeof this.fd != 'number'){
//就当触发open时 就会再次调用
this.once('open',()=>this._write(chunk,encoding,clear,callback));
return;
}
// 真正开始写
fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,bytesHasWrite)=>{
// 偏移量增加 大小为写入长度
this.pos += bytesHasWrite;
// 缓存长度减小 大小为写入长度
this.cacheLength -= bytesHasWrite;
if(callback)this.cbCache.push(callback);
//调用清除Buffer函数
clear();
})
}
clearBuffer(){
// 获取缓存的第一个
let buf = this.cache.shift();
// 若存在第一个
if(buf){
// 再写
// chunk、encodeing、callback全是buffer的属性
this._write(buf.chunk,buf.encoding,()=>this.clearBuffer(),buf.callback)
}else{
// 啊!!终于写完了
// 判断needDrain,当需要就触发
// 如果第一次传入数据未到达水位线
// console.log(this.needDrain);
if(this.needDrain){
// 写完了 不写了
this.iswriting = false;
// 触发过了,下次自来
this.needDrain = false;
// 第一个要在drain前触发
// 因为无论如何都写了
// 将cbCache第一项shift出来并执行
if(first){
// 先触发drain
this.emit('drain');
this.cbCache.shift()();
}else{
// 先触发一次回调
this.cbCache.shift()();
this.emit('drain');
}
}
// 执行所有callback
// this.cbCache.shift();
this.cbCache.map(fn=>fn());
//清空,下次再来
this.cbCache = [];
}
}
}
// options处理,来自源码
// 深拷贝,不改变原输入
function copyObject(source) {
var target = {};
for (var key in source)
target[key] = source[key];
return target;
}
// 判断options,使options合法化,返回options
function getOptions(options, defaultOptions) {
// 传入null或者undefined,返回默认配置defaultOptions
if (options === null || options === undefined || typeof options === 'function') {
return defaultOptions;
}
// 如果是字符串,默options认为编码
if (typeof options === 'string') {
// 源代码用util._extend,已废弃,用Object.assign()代替
// 用Object.assign()强制把defaultOptions变成json格式
// 甚至你的defaultOptions是个函数(笑
defaultOptions = Object.assign({}, defaultOptions);
defaultOptions.encoding = options;
// e.g. options.encoding = 'utf8'
options = defaultOptions;
} else if (typeof options !== 'object') {
throw new TypeError(`"options" must be a string or an object, got ${typeof options} instead.`);
}
// 如果options.encoding不是'buffer',则判断所输入encoding是否合法
if (options.encoding !== 'buffer') assertEncoding(options.encoding);
return options;
}
// 判断encoding是否合法的函数
function assertEncoding(encoding) {
// Buffer.isEncoding(),存在如utf8,则返回true
if (encoding && !Buffer.isEncoding(encoding)) {
throw new Error(`Unknown encoding: ${encoding}`);
}
}
module.exports = WriteStream;
但是和源码还是略有差别
可读流
可读流就简单很多
无非是触发 data 开始读
pause方法会阻断可读流
resume方法使可读流继续
let fs = require('fs');
let EventEmiter = require('events');
let first = undefined;
let firstFlag = true;
/**
* --------------------------------------------------
* Node.js源代码中
* ReadStream继承(call)Readable
* Readable继承(call)Stream
* Stream继承(call)EE(EventEmiter)
* 是一个链式关系
* 所以我们直接继承EventEmiter
* 并且将Readable中的read等方法写在WriteStream中
*
* @param {string} path 地址
* @param {object} options 配置
* @extends EventEmiter
* @this ReadStream
* --------------------------------------------------
*/
class ReadStream extends EventEmiter {
constructor(path, options) {
super();
// 设置options配置
options = copyObject(getOptions(options, {}));
// path必须是string类型
if (typeof path !== 'string') throw Error('path must be string');
// 地址
this.path = path;
// 文件标识符
this.fd = options.fd === undefined ? null : options.fd;
// 默认r
this.flags = options.flags === undefined ? 'r' : options.flags;
// 默认0o666
this.mode = options.mode === undefined ? 0o666 : options.mode;
// 编码 默认utf8
this.encoding = options.encoding || 'utf8';
// 开始点
this.start = options.start || 0;
// 结束点
this.end = options.end || null;
// 高水位线
this.highWaterMark = options.highWaterMark || 64 * 1024;
// 是否自动关闭
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
// 偏移量 初始为起始点
this.pos = this.start;
// 是否流动,流动就读,不流动就不读
this.isFlow = null;
// 创建buffer,作为存放读到的内容的容器
this.buffer = Buffer.alloc(this.highWaterMark);
// 打开文件
this.open();
// 当用户监听流data,就开始读取
this.on('newListener', type => {
if (type === 'data') {
this.isFlow = true;
this._read();
}
})
}
// 打开文件
open() {
fs.open(this.path, this.flags, (err, fd) => {
// 如果报错
if (err) {
// 触发error事件,传入err
this.emit('error', err);
// 是否自动关闭文件?
if (this.autoClose) {
// 关闭文件
this.destory();
}
return;
}
// 获取目标fd
this.fd = fd;
// 触发open事件
this.emit('open');
})
}
// 关闭文件
destory() {
// fd存在,表示已经打开文件
if (typeof this.fd === 'number') {
// 关闭文件
fs.close(this.fd, () => {
// 清空fd
this.fd = null;
// 触发close
this.emit('close');
});
return;
}
// fd不存在,文件未打开,触发close
this.emit('close');
}
// 内部读的接口
_read() {
// 如果还未打开
if (typeof this.fd !== 'number') {
this.once('open', () => {
this._read();
});
return;
}
// 已经打开了
// readNum是所读长度,当this.end存在,取高水位先和end到pos差两者最小,end不存在,默认最高水位线
let readNum = this.end ? Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark;
// 开始读
fs.read(this.fd, this.buffer, 0, readNum, this.pos, (err, bytesHasRead) => {
// 有内容
if (bytesHasRead > 0) {
// 增加偏移量
this.pos += bytesHasRead;
let result = this.buffer.slice(0, bytesHasRead);
this.emit('data',
this.encoding ? result.toString(this.encoding) : result
);
// 开允许读,就继续读
if (this.isFlow) {
this._read();
}
} else {
// 没内容了 触发end
this.emit('end');
this.destory();
}
});
}
// 暂停!
pause() {
this.isFlow = false;
}
// 开始
resume() {
// 确认是否打开了文件 以免空读 报错
if (this.fd) {
this.isFlow = true;
this._read();
}
}
}
// options处理 来自源码
function copyObject(source) {
var target = {};
for (var key in source)
target[key] = source[key];
return target;
}
function getOptions(options, defaultOptions) {
if (options === null || options === undefined || typeof options === 'function') {
return defaultOptions;
}
if (typeof options === 'string') {
defaultOptions = Object.assign({}, defaultOptions);
defaultOptions.encoding = options;
options = defaultOptions;
} else if (typeof options !== 'object') {
throw new TypeError(`"options" must be a string or an object, got ${typeof options} instead.`);
}
if (options.encoding !== 'buffer') assertEncoding(options.encoding);
return options;
}
function assertEncoding(encoding) {
if (encoding && !Buffer.isEncoding(encoding)) {
throw new Error(`Unknown encoding: ${encoding}`);
}
}
module.exports = ReadStream;
let fs = require('fs');
let ReadStream = require('./myReadStream')
let rs = new ReadStream('./target.txt',{
highWaterMark:3,
autoClose:true,
flags:'r',
start:3,
end: 14,
encoding:'utf8'
});
rs.on('open',function () {
console.log('open')
});
rs.on('data',function (data) {
console.log(data);
//暂停
rs.pause();
});
rs.on('end',function () {
console.log('完毕')
})
rs.on('close',function () {
console.log('close');
clearInterval(timer);
})
var timer = setInterval(() => {
// 每隔300ms继续一次
rs.resume();
}, 300);
target内容为“12345678901234567890”
输出
open
456
789
012
345
完毕
close
可读流就写好了