渴望力量嗎?少年!流的原理
流(stream),看一個人流不流逼,就看你對流的理解了
學習本無底,前進莫徬徨
今天跟大家分享的是node.js中的流(stream
)。它的作用大家應該都在平常使用node的時候看到過,比如:
gulp
中的pipe就是流的一種方法,透過可寫流和可讀流的配合,達到不佔用多餘快取的一種讀寫方式。express和koa中的res和req也是流,res是
可寫流
,req是可讀流
,他們都是透過封裝node中的net模組的socket(雙工流
,即可寫、可讀流)而來的。。。。
可能很多時候大家都知道怎麼用,但不瞭解它的原理,很尷尬,就像這樣
image
何謂流?
流是一組有序的,有起點和終點的位元組資料傳輸手段。
它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理。
流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。
流被分為
Readable
(可讀流)、Writable
(可寫流)、Duplex
(雙工流)、Transform
(轉換流)
流中的是什麼?
二進位制模式
:每個分塊都是buffer、string物件。物件模式
:流內部處理的是一系列普通物件。
可讀流
可讀流分為
flowing
和paused
兩種模式
引數
path
:讀取的檔案的路徑-
option
:highWaterMark
:水位線,一次可讀的位元組,一般預設是64k
flags
:標識,開啟檔案要做的操作,預設是r
encoding
:編碼,預設為bufferstart
:開始讀取的索引位置end
:結束讀取的索引位置(包括結束位置)autoClose
:讀取完畢是否關閉,預設為true
let ReadStream = require('./ReadStream')//讀取的時候預設讀64k let rs = new ReadStream('./a.txt',{ highWaterMark: 2,//一次讀的位元組 預設64k flags: 'r', //標示 r為讀 w為寫 autoClose: true, //預設讀取完畢後自動關閉 start: 0, end: 5, //流是閉合區間包start,也包end 預設是讀完 encoding: 'utf8' //預設編碼是buffer})
方法
data
:切換到流動模式,可以流出資料
rs.on('data', function (data) { console.log(data); });
open
:流開啟檔案的時候會觸發此監聽
rs.on('open', function () { console.log('檔案被開啟'); });
error
:流出錯的時候,監聽錯誤資訊
rs.on('error', function (err) { console.log(err); });
end
:流讀取完成,觸發end
rs.on('end', function (err) { console.log('讀取完成'); });
close
:關閉流,觸發
rs.on('close', function (err) { console.log('關閉'); });
pause
:暫停流(改變流的flowing,不讀取資料了);resume
:恢復流(改變流的flowing,繼續讀取資料)
//流透過一次後,停止流動,過了2s後再動rs.on('data', function (data) { rs.pause(); console.log(data); }); setTimeout(function () { rs.resume(); },2000);
fs.read()
:可讀流底層呼叫的就是這個方法,最原生的讀方法
//fd檔案描述符,一般透過fs.open中獲取//buffer是讀取後的資料放入的快取目標//0,從buffer的0位置開始放入//BUFFER_SIZE,每次放BUFFER_SIZE這麼長的長度//index,每次從檔案的index的位置開始讀//bytesRead,真實讀到的個數fs.read(fd,buffer,0,BUFFER_SIZE,index,function(err,bytesRead){ })
那讓我們自己來實現一個可愛
的讀流吧!
image
let fs = require('fs')let EventEmitter = require('events')class ReadStream extends EventEmitter{ constructor(path,options = {}){ super() this.path = path this.highWaterMark = options.highWaterMark || 64*1024 this.flags = options.flags || 'r' this.start = options.start || 0 this.pos = this.start //會隨著讀取的位置改變 this.autoClose = options.autoClose || true this.end = options.end || null //預設null就是buffer this.encoding = options.encoding || null //引數的問題 this.flowing = null //非流動模式 //建立個buffer用來儲存每次讀出來的資料 this.buffer = Buffer.alloc(this.highWaterMark) //開啟這個檔案 this.open() //此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件 this.on('newListener',(type)=>{// 等待著他監聽data事件 if(type === 'data'){ this.flowing = true //開始讀取 客戶已經監聽的data事件 this.read() } }) } //預設第一次呼叫read方法時fd還沒獲取 所以不能直接讀 read(){ if(typeof this.fd != 'number'){ //等待著觸發open事件後fd肯定拿到了 再去執行read方法 return this.once('open',()=>{this.read()}) } //每次讀的時候都要判斷一下下次讀幾個 如果沒有end就根據highWaterMark來(讀所有的) 如果有且大於highWaterMark就根據highWaterMark來 如果小於highWaterMark就根據end來 let howMuchToRead = this.end?Math.min(this.end - this.pos + 1,this.highWaterMark):this.highWaterMark fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,byteRead)=>{ this.pos += byteRead let b = this.encoding?this.buffer.slice(0,byteRead).toString(this.encoding):this.buffer.slice(0,byteRead) this.emit('data',b) //如果讀取到的數量和highWaterMark一樣 說明還得繼續讀 if((byteRead === this.highWaterMark)&&this.flowing){ this.read() } if(byteRead { this.emit('close') }) } pause(){ this.flowing = false } resume(){ this.flowing = true this.read() } open(){ //fd表示的就是當前this.path的這個檔案,從3開始(number型別) fs.open(this.path,this.flags,(err,fd)=>{ //有可能fd這個檔案不存在 需要做處理 if(err){ //如果有自動關閉 則幫他銷燬 if(this.autoClose){ //銷燬(關閉檔案,觸發關閉檔案事件) this.destory() } //如果有錯誤 就會觸發error事件 this.emit('error',err) return } //儲存檔案描述符 this.fd = fd //當檔案開啟成功時觸發open事件 this.emit('open',this.fd) }) } }
Readable
這個方法是可讀流的一種
暫停模式
,他的模式可以參考為讀流是往水杯倒水的人,Readable是喝水的人,他們之間存在著一種聯絡,只要Readable喝掉一點水,讀流就會繼續往裡倒
。
Readable是什麼?
他會在剛開始監聽Readable的時候就觸發流的,此時流就會讀取一次資料,之後
流會監聽,如果有人讀過流(喝過水),並且減少,就會再去讀一次(倒點水)
主要可以用來做
行讀取器(LineReader)
let fs = require('fs')let read = require('./ReadableStream')let rs = fs.createReadStream('./a.txt', { //每次讀7個 highWaterMark: 7})//如果讀流第一次全部讀下來並且小於highWaterMark,就會再讀一次(再觸發一次readable事件)//如果rs.read()不加引數,一次性讀完,會從快取區再讀一次,為null//如果readable每次都剛好讀完(即rs.read()的引數剛好和highWaterMark相等),就會一直觸發readable事件,如果最後不足他想喝的數,他就會先觸發一次null,最後把剩下的喝完//一開始快取區為0的時候也會預設調一次readable事件rs.on('readable', () => { let result = rs.read(2) console.log(result) })
實戰:行讀取器(平常我們的檔案可能有回車、換行,此時如果要每次想讀一行的資料,就得用到readable
)
let EventEmitter = require('events')//如果要將內容全部讀出就用on('data'),精確讀取就用on('readable')class LineReader extends EventEmitter { constructor(path) { super() this.rs = fs.createReadStream(path) //回車符的十六進位制 let RETURN = 0x0d //換行符的十六進位制 let LINE = 0x0a let arr = [] this.on('newListener', (type) => { if (type === 'newLine') { this.rs.on('readable', () => { let char //每次讀一個,當讀完的時候會返回null,終止迴圈 while (char = this.rs.read(1)) { switch (char[0]) { case RETURN: break; //Mac下只有換行符,windows下是回車符和換行符,需要根據不同的轉換。因為我這裡是Mac case LINE: //如果是換行符就把陣列轉換為字串 let r = Buffer.from(arr).toString('utf8') //把陣列清空 arr.length = 0 //觸發newLine事件,把得到的一行資料輸出 this.emit('newLine', r) break; default: //如果不是換行符,就放入陣列中 arr.push(char[0]) } } }) } }) //以上只能取出之前的換行符前的程式碼,最後一行的後面沒有換行符,所以需要特殊處理。當讀流讀完需要觸發end事件時 this.rs.on('end', () => { //取出最後一行資料,轉成字串 let r = Buffer.from(arr).toString('utf8') arr.length = 0 this.emit('newLine', r) }) } }let lineReader = new LineReader('./a.txt') lineReader.on('newLine', function (data) { console.log(data) })
那麼Readable到底是怎樣的存在呢?我們接下來實現他的原始碼,看看內部到底怎麼回事
let fs = require('fs')let EventEmitter = require('events')class ReadStream extends EventEmitter{ constructor(path,options = {}){ super() this.path = path this.highWaterMark = options.highWaterMark || 64*1024 this.flags = options.flags || 'r' this.start = options.start || 0 this.pos = this.start //會隨著讀取的位置改變 this.autoClose = options.autoClose || true this.end = options.end || null //預設null就是buffer this.encoding = options.encoding || null //引數的問題 this.reading = false //非流動模式 //建立個buffer用來儲存每次讀出來的資料 this.buffers = [] //快取區長度 this.len = 0 //是否要觸發readable事件 this.emittedReadable = false //觸發open獲取檔案的fd識別符號 this.open() //此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件 this.on('newListener',(type)=>{// 等待著他監聽data事件 if(type === 'readable'){ //開始讀取 客戶已經監聽的data事件 this.read() } }) } //readable真正的原始碼中的方法,計算出和n最接近的2的冪次數 computeNewHighWaterMark(n) { n--; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; n++; return n; } read(n){ //當讀的數量大於水平線,會透過取2的冪次取比他大和最接近的數 if(this.len 0 && n,buffer,buffer] //每次取出快取前的第一個buffer while(flag && (buf = this.buffers.shift())){ for(let i=0;i{this._read()}) } //先讀這麼多buffer let buffer = Buffer.alloc(this.highWaterMark) fs.read(this.fd,buffer,0,buffer.length,this.pos,(err,byteRead)=>{ if(byteRead > 0){ //當第一次讀到資料後,改變reading的狀態,如果觸發read事件,可能還會在觸發第二次_read this.reading = false //每次讀到資料增加快取取得長度 this.len += byteRead //每次讀取之後,會增加讀取的檔案的讀取開始位置 this.pos += byteRead //將讀到的buffer放入快取區buffers中 this.buffers.push(buffer.slice(0,byteRead)) //觸發readable if(this.emittedReadable){ this.emittedReadable = false //可以讀取了,預設開始的時候杯子填滿了 this.emit('readable') } }else{ //沒讀到就出發end事件 this.emit('end') } }) } destory(){ if(typeof this.fd != 'number'){ return this.emit('close') } //如果檔案被開啟過 就關閉檔案並且觸發close事件 fs.close(this.fd,()=>{ this.emit('close') }) } open(){ //fd表示的就是當前this.path的這個檔案,從3開始(number型別) fs.open(this.path,this.flags,(err,fd)=>{ //有可能fd這個檔案不存在 需要做處理 if(err){ //如果有自動關閉 則幫他銷燬 if(this.autoClose){ //銷燬(關閉檔案,觸發關閉檔案事件) this.destory() } //如果有錯誤 就會觸發error事件 this.emit('error',err) return } //儲存檔案描述符 this.fd = fd //當檔案開啟成功時觸發open事件 this.emit('open',this.fd) }) } }
Readable和讀流的data的區別就是,Readable可以控制自己從快取區讀多少和控制讀的次數,而data是每次讀取都清空快取,讀多少輸出多少
我們可以看一下下面這個例子
let rs = fs.createReadStream('./a.txt') rs.on('data',(data)=>{ console.log(data) })//因為上面的data事件把資料讀了,清空快取區。所以導致下面的readable讀出為nullrs.on('readable',()=>{ let result = r.read(1) console.log(result) })
自定義可讀流
因為createReadStream
內部呼叫了ReadStream
類,ReadStream
又實現了Readable
介面,ReadStream
實現了_read()
方法,所以我們透過自定義一個類繼承stream
模組的Readable
,並在原型
上自定義一個_read()
就可以自定義自己的可讀流
let { Readable } = require('stream')class MyRead extends Readable{ //流需要一個_read方法,方法中push什麼,外面就接收什麼 _read(){ //push方法就是上面_read方法中的push一樣,把資料放入快取區中 this.push('100') //如果push了null就表示沒有東西可讀了,停止(如果不寫,就會一直push上面的值,死迴圈) this.push(null) } }
可寫流
如果檔案不存在會建立,如果有內容會被清空
讀取到highWaterMark的時候就會輸出
第一次是真的寫到檔案 後面就是寫入快取區 再從快取區裡面去取
引數(和可讀流的類似)
path
:寫入的檔案的路徑-
option
:highWaterMark
:水位線,一次可寫入快取中的位元組,一般預設是64k
flags
:標識,寫入檔案要做的操作,預設是w
encoding
:編碼,預設為bufferstart
:開始寫入的索引位置end
:結束寫入的索引位置(包括結束位置)autoClose
:寫入完畢是否關閉,預設為true
let ReadStream = require('./ReadStream')//讀取的時候預設讀64k let rs = new ReadStream('./a.txt',{ highWaterMark: 2,//一次讀的位元組 預設64k flags: 'r', //標示 r為讀 w為寫 autoClose: true, //預設讀取完畢後自動關閉 start: 0, end: 5, //流是閉合區間包start,也包end 預設是讀完 encoding: 'utf8' //預設編碼是buffer})
方法
write
let fs = require('fs')let ws = fs.createWriteStream('./d.txt',{ flags: 'w', encoding: 'utf8', start: 0, //write的highWaterMark只是用來觸發是不是幹了 highWaterMark: 3 //寫是預設16k})//返回boolean 每當write一次都會在ws中吃下一個饅頭 當吃下的饅頭數量達到highWaterMark時 就會返回false 吃不下了會把其餘放入快取 其餘狀態返回true//write只能放string或者bufferflag = ws.write('1','utf8',()=>{ console.log(i) })
drain
//drain只有嘴塞滿了 吃完(包括記憶體中的,就是地下的)才會觸發 這裡是兩個條件 一個是必須是吃下highWaterMark個饅頭 並且在吃完的時候才會callbackws.on('drain',()=>{ console.log('幹了') })
image
fs.write()
:可讀流底層呼叫的就是這個方法,最原生的讀方法
//wfd檔案描述符,一般透過fs.open中獲取//buffer,要取資料的快取源//0,從buffer的0位置開始取//BUFFER_SIZE,每次取BUFFER_SIZE這麼長的長度//index,每次寫入檔案的index的位置//bytesRead,真實寫入的個數fs.write(wfd,buffer,0,bytesRead,index,function(err,bytesWrite){ })
透過程式碼實現
let fs = require('fs')let EventEmitter = require('events')//只有第一次write的時候直接用_write寫入檔案 其餘都是放到cache中 但是len超過了highWaterMark就會返回false告知需要drain 很佔快取//從第一次的_write開始 回去一直透過clearBuffer遞迴_write寫入檔案 如果cache中沒有了要寫入的東西 會根據needDrain來判斷是否觸發乾點class WriteStream extends EventEmitter{ constructor(path,options = {}){ super() this.path = path this.highWaterMark = options.highWaterMark || 64*1024 this.flags = options.flags || 'r' this.start = options.start || 0 this.pos = this.start this.autoClose = options.autoClose || true this.mode = options.mode || 0o666 //預設null就是buffer this.encoding = options.encoding || null //開啟這個檔案 this.open() //寫檔案的時候需要哪些引數 //第一次寫入的時候 是給highWaterMark個饅頭 他會硬著頭皮寫到檔案中 之後才會把多餘吃不下的放到快取中 this.writing = false //快取陣列 this.cache = [] this.callbackList = [] //陣列長度 this.len = 0 //是否觸發drain事件 this.needDrain = false } clearBuffer(){ //取快取中最上面的一個 let buffer = this.cache.shift() if(buffer){ //有buffer的情況下 this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer(),buffer.callback) }else{ //沒有的話 先看看需不需要drain if(this.needDrain){ //觸發drain 並初始化所有狀態 this.writing = false this.needDrain = false this.callbackList.shift()() this.emit('drain') } this.callbackList.map(v=>{ v() }) this.callbackList.length = 0 } } _write(chunk,encoding,clearBuffer,callback){ //因為write方法是同步呼叫的 所以可能還沒獲取到fd if(typeof this.fd != 'number'){ //直接在open的時間物件上註冊一個一次性事件 當open被emit的時候會被呼叫 return this.once('open',()=>this._write(chunk,encoding,clearBuffer,callback)) } fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWrite)=>{ this.pos += byteWrite //每次寫完 相應減少記憶體中的數量 this.len -= byteWrite if(callback) this.callbackList.push(callback) //第一次寫完 clearBuffer() }) } //寫入方法 write(chunk,encoding=this.encoding,callback){ //判斷chunk必須是字串或者buffer 為了統一都變成buffer chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding) //維護快取的長度 3 this.len += chunk.length let ret = this.len this.clearBuffer(),callback) } // console.log(ret) //能不能繼續寫了 false代表下次寫的時候更佔記憶體 return ret } destory(){ if(typeof this.fd != 'number'){ return this.emit('close') } //如果檔案被開啟過 就關閉檔案並且觸發close事件 fs.close(this.fd,()=>{ this.emit('close') }) } open(){ //fd表示的就是當前this.path的這個檔案,從3開始(number型別) fs.open(this.path,this.flags,(err,fd)=>{ //有可能fd這個檔案不存在 需要做處理 if(err){ //如果有自動關閉 則幫他銷燬 if(this.autoClose){ //銷燬(關閉檔案,出發關閉檔案事件) this.destory() } //如果有錯誤 就會觸發error事件 this.emit('error',err) return } //儲存檔案描述符 this.fd = fd //當檔案開啟成功時觸發open事件 this.emit('open',this.fd) }) } }
自定義可寫流
因為createWriteStream
內部呼叫了WriteStream
類,WriteStream
又實現了Writable
介面,WriteStream
實現了_write()
方法,所以我們透過自定義一個類繼承stream
模組的Writable
,並在原型
上自定義一個_write()
就可以自定義自己的可寫流
let { Writable } = require('stream')class MyWrite extends Writable{ _write(chunk,encoding,callback){ //write()的第一個引數,寫入的資料 console.log(chunk) //這個callback,就相當於我們上面的clearBuffer方法,如果不執行callback就不會繼續從快取中取出寫 callback() } }let write = new MyWrite() write.write('1','utf8',()=>{ console.log('ok') })
pipe
管道流,是可讀流上的方法,至於為什麼放到這裡,主要是因為需要2個流的基礎知識,是可讀流配合可寫流的一種
傳輸方式
。如果用原來的讀寫,因為寫比較耗時
,所以會多讀少寫
,耗記憶體
,但用了pipe
就不會了,始終用規定
的記憶體。
用法
let fs = require('fs')//pipe方法叫管道 可以控制速率let rs = fs.createReadStream('./d.txt',{ highWaterMark: 4})let ws = fs.createWriteStream('./e,txt',{ highWaterMark: 1})//會監聽rs的on('data')將讀取到的資料,透過ws.write的方法寫入檔案//呼叫寫的一個方法 返回boolean型別//如果返回false就呼叫rs的pause方法 暫停讀取//等待可寫流 寫入完畢在監聽drain resume rsrs.pipe(ws) //會控制速率 防止淹沒可用記憶體
自己實現一下
let fs = require('fs')//這兩個是上面自己寫的ReadStream和WriteStreamlet ReadStream = require('./ReadStream')let WriteStream = require('./WriteStream')//如果用原來的讀寫,因為寫比較耗時,所以會多讀少寫,耗記憶體ReadStream.prototype.pipe = function(dest){ this.on('data',(data)=>{ let flag = dest.write(data) //如果寫入的時候嘴巴吃滿了就不繼續讀了,暫停 if(!flag){ this.pause() } }) //如果寫的時候嘴巴里的吃完了,就會繼續讀 dest.on('drain',()=>{ this.resume() }) this.on('end',()=>{ this.destory() //清空快取中的資料 fs.fsync(dest.fd,()=>{ dest.destory() }) }) }
雙工流
有了雙工流,我們可以在同一個物件上
同時實現可讀和可寫
,就好像同時繼承這兩個介面。 重要的是雙工流的可讀性和可寫性操作完全獨立
於彼此。這僅僅是將兩個特性組合成一個物件。
let { Duplex } = require('stream')//雙工流,可讀可寫class MyDuplex extends Duplex{ _read(){ this.push('hello') this.push(null) } _write(chunk,encoding,clearBuffer){ console.log(chunk) clearBuffer() } }let myDuplex = new MyDuplex()//process.stdin是node自帶的process程式中的可讀流,會監聽命令列的輸入//process.stdout是node自帶的process程式中的可寫流,會監聽並輸出在命令列中//所以這裡的意思就是在命令列先輸出hello,然後我們輸入什麼他就出來對應的buffer(先作為可讀流出來)process.stdin.pipe(myDuplex).pipe(process.stdout)
轉換流
轉換流的輸出是從輸入中
計算
出來的。對於轉換流,我們不必實現read
或write
的方法,我們只需要實現一個transform
方法,將兩者結合起來。它有write
方法的意思,我們也可以用它來push
資料。
let { Transform } = require('stream')class MyTransform extends Transform{ _transform(chunk,encoding,callback){ console.log(chunk.toString().toUpperCase()) callback() } }let myTransform = new MyTransform()class MyTransform2 extends Transform{ _transform(chunk,encoding,callback){ console.log(chunk.toString().toUpperCase()) this.push('1') // this.push(null) callback() } }let myTransform2 = new MyTransform2()//此時myTransform2被作為可寫流觸發_transform,輸出輸入的大寫字元後,會透過可讀流push字元到下一個轉換流中//當寫入的時候才會觸發transform的值,此時才會push,所以後面的pipe拿到的chunk是前面的push的值process.stdin.pipe(myTransform2).pipe(myTransform)
總結
可讀流
在 flowing 模式下, 可讀流自動從系統底層讀取資料,並透過 EventEmitter 介面的事件儘快將資料提供給應用。
在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。
-
所有初始工作模式為 paused 的 Readable 流,可以透過下面三種途徑切換到 flowing 模式:
監聽 'data' 事件
呼叫 stream.resume() 方法
呼叫 stream.pipe() 方法將資料傳送到 Writable
-
可讀流可以透過下面途徑切換到 paused 模式:
如果不存在管道目標(pipe destination),可以透過呼叫 stream.pause() 方法實現。
如果存在管道目標,可以透過取消 'data' 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。
可寫流
需要知道只有在
嘴
真正的吃滿了,並且等到把嘴裡的和地上的饅頭(快取中的)都吃下了才會觸發drain
事件第一次寫入會直接寫入檔案中,後面會從快取中一個個取
雙工流
只是對可寫可讀流的一種應用,既可作為可讀流,也能作為可寫流,並且作為可讀或者可寫時時
隔離
的
轉換流
一般轉換流是邊輸入邊輸出的,而且一般只有觸發了寫入操作時才會進入
_transform
方法中。跟雙工流的區別就是,他的可讀可寫是在一起
的。
OK,講完收工,從此你就是流
魔王
image
作者:Shinemax
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2325/viewspace-2813007/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 少年,你渴望超程式設計的力量嗎?——symbol程式設計Symbol
- Java 8 的Stream流那麼強大,你知道它的原理嗎?Java
- 我渴望的insert操作!
- 這些手寫程式碼會了嗎?少年
- Laravel 管道流原理Laravel
- 谷歌想用Chrome的力量消滅網址,它能做到嗎?谷歌Chrome
- nodeJs流的使用及原理NodeJS
- 鉗形電流表測試電流的原理介紹
- 抽象的力量抽象
- 自由職業者渴望得到的10個工具
- 工作流引擎的工作原理與功能
- 節流原理以及實現
- 開源的力量
- Kafka 的這些原理你知道嗎Kafka
- 你知道[ ].slice.call()的原理嗎?
- 你懂RocketMQ 的架構原理嗎?MQ架構
- 防抖和節流原理分析
- 淺析瀑布流佈局原理
- 流媒體 Buffer 設計原理
- CAN匯流排原理_學習
- API的宣告性力量API
- 這裡有研究工作流osworkflow的嗎?
- SSL證書的工作原理你知道嗎?
- IIC序列匯流排的組成及工作原理
- Flink流處理過程的部分原理分析
- 你知道資料庫索引的工作原理嗎?資料庫索引
- Nest Energy:智慧家居的力量
- 開源的四股力量
- Flink流計算中SQL表的概念和原理SQL
- 說說Node.js中 流 的一些原理Node.js
- 純CSS實現瀑布流,你會嗎?CSS
- NodeJS stream 流 原理分析(附原始碼)NodeJS原始碼
- PCB板上可以走100A的電流嗎?大電流路徑設定技巧
- 用 HTTPS 安全嗎?HTTPS 的原理是啥?HTTP
- JS的節流、函式防抖 原理及使用場景JS函式
- 解鎖 AB 測試的力量
- 開源表單工作流引擎好用嗎?
- Java基礎 | Stream流原理與用法總結Java