Por qué usar secuencias de archivos
Imagine tal escenario, quiero procesar un archivo 10G, pero el tamaño de mi memoria es solo 2G, ¿qué debo hacer?
Podemos leer el archivo 5 veces, y solo leer 2G de datos cada vez, para que este problema pueda resolverse, ¡entonces el proceso de lectura en segmentos es continuo!
En el nodo stream
, el módulo encapsula las operaciones básicas de la secuencia, y la secuencia de archivos también depende directamente de este módulo. Aquí usamos la secuencia de archivos para comprender en profundidadstream
flujo de archivo legible
Lea el archivo y lea el contenido del archivo en la memoria poco a poco.
Cómo utilizar
Echemos un vistazo al uso básico primero.
const fs = require('fs')
const rs = fs.createReadStream('./w-test.js')
rs.on('data', (chunk) => {
console.log(chunk)
})
rs.on('close', () => {
console.log('close')
})
Como se muestra en el código anterior, hemos fs.createStream()
creado una secuencia legible para leer el archivo w-test.js .
En ese on('data')
momento , los datos del archivo se leerán automáticamente y el contenido de 64 kb se leerá de forma predeterminada cada vez, highWaterMark
y el valor de umbral de cada flujo de contenido también se puede cambiar dinámicamente a través de parámetros.
close
El evento se activa automáticamente cuando se lee el archivo .
El siguiente código es el parámetro que createReadStream
se puede configurar
const rs = fs.createReadStream('./w-test.js', {
flags: 'r', // 文件系统表示,这里是指以可读的形式操作文件
encoding: null, // 编码方式
autoClose: false, // 读取完毕时是否自动触发 close 事件
start: 0, // 开始读取的位置
end: 2, // 结束读取的位置
highWaterMark: 2 // 每次读取的内容大小
})
Nota: Se incluyen tanto el inicio como el final, es decir, [inicio, final].
De hecho, fs.crateReadStream
devuelve una instancia de fs.ReadStream
la clase , por lo que el código anterior es equivalente a:
const rs = new fs.ReadStream('./w-test.js', {
flags: 'r', // 文件系统表示,这里是指以可读的形式操作文件
encoding: null, // 编码方式
autoClose: false, // 读取完毕时是否自动触发 close 事件
start: 0, // 开始读取的位置
end: 2, // 结束读取的位置
highWaterMark: 2 // 每次读取的内容大小
})
Implementación de transmisiones legibles por archivos
Después de comprender el uso, deberíamos intentar hacerlo en principio.Luego, escribimos una secuencia legible a mano.
fs.read / fs.abrir
La esencia de un flujo legible es leer los datos del archivo en lotes, y fs.read()
el método puede controlar el tamaño del contenido del archivo leído.
fs.read(fd, buffer, offset, length, position, callback)
fd: el descriptor de archivo para leer
búfer: el búfer donde se escribirán los datos (escriba el contenido del archivo leído en este búfer)
offset: el desplazamiento para comenzar a escribir en el búfer (desde el índice del búfer para comenzar a escribir)
longitud: el número de bytes leídos (leer varios bytes del archivo)
posición: especifique la posición para comenzar a leer desde el archivo (comience a leer desde los primeros bytes del archivo)
devolución de llamada: función de devolución de llamada
-
errar
-
bytesRead: el número real de bytes leídos
Leer un archivo requiere un identificador de archivo, deberíamos usarlo fs.open
para obtener
ruta: ruta del archivo
banderas: banderas del sistema de archivos, por defecto: 'r'. Significa qué operación hacer en el archivo, las comunes son las siguientes:
-
r: abre el archivo para leer
-
w: abre el archivo para escribir
-
a: abra el archivo para anexar
modo: permiso de operación de archivo, valor predeterminado: 0o666 (legible y escribible).
devolución de llamada: función de devolución de llamada. Los parámetros llevados en la función son los siguientes:
-
err: si falla, el valor es el motivo del error
-
fd (número): descriptor de archivo, este valor se usa al leer y escribir archivos
inicialización
En primer lugar, ReadStream
es una clase.Desde el punto de vista del rendimiento, esta clase puede escuchar eventos on('data')
, por lo que debemos dejar que herede EventEmitter
del siguiente código:
class ReadStream extends EventEmitter {
constructor() {
super();
}
}
Luego inicializamos los parámetros y abrimos el archivo, el siguiente código (el código clave se comentará en el código):
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super()
// 解析参数
this.path = path
this.flags = options.flags ?? 'r'
this.encoding = options.encoding ?? 'utf8'
this.autoClose = options.autoClose ?? true
this.start = options.start ?? 0
this.end = options.end ?? undefined
this.highWaterMark = options.highWaterMark ?? 16 * 1024
// 文件的偏移量
this.offset = this.start
// 是否处于流动状态,调用 pause 或 resume 方法时会用到,下文会讲到
this.flowing = false
// 打开文件
this.open()
// 当绑定新事件时会触发 newListener
// 这里当绑定 data 事件时,自动触发文件的读取
this.on('newListener', (type) => {
if (type === 'data') {
// 标记为开始流动
this.flowing = true
// 开始读取文件
this.read()
}
})
}
}
Antes de leer el archivo, tenemos que abrir el archivo, es decir
this.open()
, .on('newListener')
Es un evento de EventEmitter, que se activará cada vez que vinculamos un nuevo eventonewListener
, por ejemplo: cuandoon('data')
nosotros ,newListener
el evento se activará y el tipo es 'datos'.Aquí, cuando escuchamos
data
el enlace del evento (es deciron('data')
), comenzamos a leer el archivothis.read()
, esthis.read()
decir, nuestro método principal.
abierto
open
Métodos de la siguiente manera:
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
// 文件打开失败触发 error 事件
this.emit('error', err)
return
}
// 记录文件标识符
this.fd = fd
// 文件打开成功后触发 open 事件
this.emit('open')
})
}
Registre el identificador del archivo cuando se abre el archivo, es decirthis.fd
leer
read
Métodos de la siguiente manera:
read() {
// 由于 ```fs.open``` 是异步操作,
// 所以当调用 read 方法时,文件可能还没有打开
// 所以我们要等 open 事件触发之后,再次调用 read 方法
if (typeof this.fd !== 'number') {
this.once('open', () => this.read())
return
}
// 申请一个 highWaterMark 字节的 buffer,
// 用来存储从文件读取的内容
const buf = Buffer.alloc(this.highWaterMark)
// 开始读取文件
// 每次读取时,都记录下文件的偏移量
fs.read(this.fd, buf, 0, buf.length, this.offset, (err, bytesRead) => {
this.offset += bytesRead
// bytesRead 为实际读取的文件字节数
// 如果 bytesRead 为 0,则代表没有读取到内容,即读取完毕
if (bytesRead) {
// 每次读取都触发 data 事件
this.emit('data', buf.slice(0, bytesRead))
// 如果处于流动状态,则继续读取
// 这里当调用 pause 方法时,会将 this.flowing 置为 false
this.flowing && this.read()
} else {
// 读取完毕后触发 end 事件
this.emit('end')
// 如果可以自动关闭,则关闭文件并触发 close 事件
this.autoClose && fs.close(this.fd, () => this.emit('close'))
}
})
}
Cada línea de código anterior tiene comentarios, creo que no es difícil de entender, aquí hay algunos puntos clave a los que prestar atención
Debe esperar a que se abra el archivo antes de poder comenzar a leer el archivo, pero la apertura del archivo es una operación asincrónica y no sabemos el tiempo específico de finalización de la apertura, por lo que activaremos un
on('open')
eventoopen
active la rellamada nuevamenteread()
fs.read()
El método se ha mencionado anteriormente, puede echar un vistazo al método principal de fs escrito a manothis.flowing
Los atributos se utilizan para determinar si es fluido y se controlarán mediante elpasue()
método yresume()
Echemos un vistazo a estos dos métodos.
pausa
pause() {
this.flowing =false
}
reanudar
resume() {
if (!this.flowing) {
this.flowing = true
this.read()
}
}
código completo
const { EventEmitter } = require('events')
const fs = require('fs')
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super()
this.path = path
this.flags = options.flags ?? 'r'
this.encoding = options.encoding ?? 'utf8'
this.autoClose = options.autoClose ?? true
this.start = options.start ?? 0
this.end = options.end ?? undefined
this.highWaterMark = options.highWaterMark ?? 16 * 1024
this.offset = this.start
this.flowing = false
this.open()
this.on('newListener', (type) => {
if (type === 'data') {
this.flowing = true
this.read()
}
})
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
return
}
this.fd = fd
this.emit('open')
})
}
pause() {
this.flowing =false
}
resume() {
if (!this.flowing) {
this.flowing = true
this.read()
}
}
read() {
if (typeof this.fd !== 'number') {
this.once('open', () => this.read())
return
}
const buf = Buffer.alloc(this.highWaterMark)
fs.read(this.fd, buf, 0, buf.length, this.offset, (err, bytesRead) => {
this.offset += bytesRead
if (bytesRead) {
this.emit('data', buf.slice(0, bytesRead))
this.flowing && this.read()
} else {
this.emit('end')
this.autoClose && fs.close(this.fd, () => this.emit('close'))
}
})
}
}
secuencia de archivos grabable
Como sugiere el nombre, escriba el contenido en el archivo poco a poco.
fs.escribir
fd: el descriptor del archivo que se va a escribir
búfer: escribe el contenido del búfer especificado en el archivo
offset: especifique la posición de escritura del búfer (escriba el contenido leído desde el índice de compensación del búfer en el archivo)
longitud: especifica el número de bytes a escribir
posición: el desplazamiento del archivo (escribir desde el byte de posición del archivo)
Cómo utilizar
// 使用方式 1:
const ws = fs.createWriteStream('./w-test.js')
// 使用方式 2:
const ws = new WriteStream('./w-test.js', {
flags: 'w',
encoding: 'utf8',
autoClose: true,
highWaterMark: 2
})
// 写入文件
const flag = ws.write('2')
ws.on('drain', () => console.log('drain'))
ws.write()
escribir en el archivo. Aquí hay un valor de retorno, que representa si se ha alcanzado el caché máximo. Cuando llamamos variaswrite()
veces sincrónicamente , el archivo no se escribe inmediatamente para cada llamada, pero solo se puede realizar una operación de escritura al mismo tiempo, por lo que el resto se escribirá en el caché hasta que se complete la última escritura. Luego, búsquelos y ejecútelos. secuencialmente desde el caché. Por lo tanto, habrá un tamaño máximo de caché en este momento, que es de 64 kb por defecto. El valor devuelto aquí representa si puede continuar escribiendo, es decir, si se ha alcanzado el caché máximo. verdadero significa que la escritura puede continuar.ws.on('drain')
, si la llamadaws.write()
devuelve false, el evento 'drain' se activa cuando se pueden seguir escribiendo datos en la secuencia.
Implementación de un flujo de escritura de archivos
inicialización
Primero define WriteStream
la clase y heredala EventEmitter
, luego inicializa los parámetros. _Presta atención a los comentarios del código_
const { EventEmitter } = require('events')
const fs = require('fs')
class WriteStream extends EventEmitter {
constructor(path, options = {}) {
super()
// 初始化参数
this.path = path
this.flags = options.flags ?? 'w'
this.encoding = options.encoding ?? 'utf8'
this.autoClose = options.autoClose ?? true
this.highWaterMark = options.highWaterMark ?? 16 * 1024
this.offset = 0 // 文件读取偏移量
this.cache = [] // 缓存的要被写入的内容
// 将要被写入的总长度,包括缓存中的内容长度
this.writtenLen = 0
// 是否正在执行写入操作,
// 如果正在写入,那以后的操作需放入 this.cache
this.writing = false
// 是否应该触发 drain 事件
this.needDrain = false
// 打开文件
this.open()
}
}
abierto()
Mismo código que ReadStream.
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
return
}
this.fd = fd
this.emit('open')
})
}
escribir()
realizar la operación de escritura
write(chunk, encoding, cb = () => {}) {
// 初始化被写入的内容
// 如果时字符串,则转为 buffer
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
// 计算要被写入的长度
this.writtenLen += chunk.length
// 判断是否已经超过 highWaterMark
const hasLimit = this.writtenLen >= this.highWaterMark
// 是否需要触发 drain
// 如果超过 highWaterMark,则代表需要触发
this.needDrain = hasLimit
// 如果没有正在写入的内容,则调用 _write 直接开始写入
// 否则放入 cache 中
// 写入完成后,调用 clearBuffer,从缓存中拿取最近一次内容开始写入
if (!this.writing) {
this.writing = true
this._write(chunk, () => {
cb()
this.clearBuffer()
})
} else {
this.cache.push({
chunk: chunk,
cb
})
}
return !hasLimit
}
// 写入操作
_write(chunk, cb) {
if (typeof this.fd !== 'number') {
this.once('open', () => this._write(chunk, cb))
return
}
// 写入文件
fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, bytesWritten) => {
if (err) {
this.emit('error', err)
return
}
// 计算偏移量
this.offset += bytesWritten
// 写入完毕,则减去当前写入的长度
this.writtenLen -= bytesWritten
cb()
})
}
Primero inicialice el contenido que se va a escribir, solo admita el búfer y la cadena; si es una cadena, se convertirá directamente en búfer.
Calcular la longitud total a escribir, es decir
this.writtenLen += chunk.length
Determinar si se ha excedido la marca de agua alta
Determinar si activar el drenaje
Determine si ya hay contenido en escritura, si no, llame
_write()
directamente para escribir, si lo hay, colóquelo en el caché. Cuando termine_write()
de escribir , llameclearBuffer()
al método para recuperar el primer contenido almacenadothis.cache
en caché y escribir. El método clearBuffer se ve así
borrar búfer ()
clearBuffer() {
// 取出缓存
const data = this.cache.shift()
if (data) {
const { chunk, cb } = data
// 继续进行写入操作
this._write(chunk, () => {
cb()
this.clearBuffer()
})
return
}
// 触发 drain
this.needDrain && this.emit('drain')
// 写入完毕,将writing置为false
this.writing = false
// needDrain 置为 false
this.needDrain = false
}
código completo
const { EventEmitter } = require('events')
const fs = require('fs')
class WriteStream extends EventEmitter {
constructor(path, options = {}) {
super()
this.path = path
this.flags = options.flags ?? 'w'
this.encoding = options.encoding ?? 'utf8'
this.autoClose = options.autoClose ?? true
this.highWaterMark = options.highWaterMark ?? 16 * 1024
this.offset = 0
this.cache = []
this.writtenLen = 0
this.writing = false
this.needDrain = false
this.open()
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
return
}
this.fd = fd
this.emit('open')
})
}
clearBuffer() {
const data = this.cache.shift()
if (data) {
const { chunk, cb } = data
this._write(chunk, () => {
cb()
this.clearBuffer()
})
return
}
this.needDrain && this.emit('drain')
this.writing = false
this.needDrain = false
}
write(chunk, encoding, cb = () => {}) {
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
this.writtenLen += chunk.length
const hasLimit = this.writtenLen >= this.highWaterMark
this.needDrain = hasLimit
if (!this.writing) {
this.writing = true
this._write(chunk, () => {
cb()
this.clearBuffer()
})
} else {
this.cache.push({
chunk: chunk,
cb
})
}
return !hasLimit
}
_write(chunk, cb) {
if (typeof this.fd !== 'number') {
this.once('open', () => this._write(chunk, cb))
return
}
fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, bytesWritten) => {
if (err) {
this.emit('error', err)
return
}
this.offset += bytesWritten
this.writtenLen -= bytesWritten
cb()
})
}
}
- FIN -
Acerca de Qi Wu Troupe
Qi Wu Troupe es el equipo front-end más grande de 360 Group y participa en el trabajo de los miembros de W3C y ECMA (TC39) en nombre del grupo. Qi Wu Troupe otorga gran importancia a la capacitación de talentos. Hay varias direcciones de desarrollo, como ingenieros, profesores, traductores, personas de interfaz comercial y líderes de equipo para que los empleados elijan, y se complementan con la capacitación técnica, profesional, general y de liderazgo correspondiente. curso Qi Dance Troupe da la bienvenida a todo tipo de talentos destacados para que presten atención y se unan a Qi Dance Troupe con una actitud abierta y de búsqueda de talentos.