您好,登錄后才能下訂單哦!
這篇“Node.js中的Buffer和事件循環實例分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Node.js中的Buffer和事件循環實例分析”文章吧。
數據的二進制
計算機中所有的內容:文字、數字、圖片、音頻、視頻最終都會使用二進制來表示
JS
可以直接去處理非常直觀的數據:比如字符串,我們通常展示給用戶的也是這些內容
但你可能會以為JS也能夠處理圖片
事實上在網頁端,圖片一直是交給瀏覽器來處理的
JS
或者HTML
,只是負責告訴瀏覽器圖片的地址
瀏覽器負責發送請求獲取這個圖片,并且最終將這個圖片給渲染出來
但是對于服務端來說是不一樣的
服務端要處理的本地文件類型相對較多
比如某一個保存文本的文件并不是使用utf-8
進行編碼的,而是用GBK
,那么我們必須讀取到他們的二進制數據,再通過GKB轉換成對應的文字
比如我們需要讀取的是一張圖片數據(二進制),再通過某些手段對圖片數據進行二次的處理(裁剪、格式轉換、旋轉、添加濾鏡),Node中有一個名為sharp
的庫,就是負責讀取圖片或者傳入圖片的Buffer
對其再進行處理的
比如在Node
中通過TCP
建立長連接,TCP傳輸的是字節流,我們需要將數據轉成字節再進行傳入,并且需要知道傳輸字節的大小(客戶端需要根據大小來判斷讀取多少內容)
Buffer和二進制
我們會發現,對于前端開發來說,通常很少會和二進制打交道,但是對于服務器端來說,為了實現很多功能,我們必須直接去操作其二進制的數據
所以Node
為了可以方便開發者完成更多功能,提供給了我們一個名為Buffer
的類,并且他是全局的
我們前面說過,Buffer中存儲的是二進制數據,那么到底是如何存儲的呢?
我們可以將Buffer看成是一個存儲二進制的數組
這個數組中的每一項,可以保存8
位二進制:00000000
,剛好是一個字節
為什么是8位呢?
在計算機中,很少的情況我們會直接操作一位二進制,因為一位二進制存儲的數據是非常有限的
所以通常會將8位合在一起作為一個單元,這個單元稱之為一個字節(byte
)
也就是說 1 byte = 8 bit
,1kb = 1024 byte
,1M = 1024kb
, 1 G = 1024 M
比如很多編程語言中的int
類型是4
個字節,long
類型是8
個字節
比如TCP
傳輸的是字節流,在寫入和讀取時都需要說明字節的個數
比如RGB
的值分別都是255
,所以本質上在計算機中都是用一個字節存儲的
Buffer和字符串
Buffer
相當于是一個字節的數組,數組中的每一項對于一個字節的大小
如果我們希望將一個字符串放入到Buffer中,是怎么樣的過程呢?
將字符串直接傳入Buffer類中,然后再創建buffer
實例
英文字符串有個特點,每一個字符對應一個字節的二進制編碼
const message = 'Hello' // 使用new關鍵字創建buffer實例,但這種創建方法已經過期了 const buffer = new Buffer(message) console.log(buffer); // <Buffer 48 65 6c 6c 6f> console.log(buffer.toString()); // Hello
中文字符串的編解碼
buffer
的默認編碼是utf-8
,所以在下列代碼中,Buffer
類是使用了utf-8編碼對我們的字符串進行編碼,使用的也是utf-8對我們的字符串進行解碼
中文字符串有個特點,在utf-8編碼中,一個文字對應3
個字節的二進制編碼
const message = '你好啊' // 使用Buffer.from對我們的字符串進行解碼 const buffer = Buffer.from(message) console.log(buffer); // <Buffer e4 bd a0 e5 a5 bd e5 95 8a> // buffer實例中有個toString方法可以對編碼進行解碼 console.log(buffer.toString()); // '你好啊'
那如果編碼和解碼用的是不同形式的編碼結果會怎么樣呢?
毫無疑問,解碼出來的東西并不是我們原先編碼的字符串
const message = '你好啊' const buffer = Buffer.from(message, 'utf16le') console.log(buffer); // <Buffer 60 4f 7d 59 4a 55> console.log(buffer.toString()); // `O}YJU
Buffer的其他創建方式
創建buffer
的方式有很多,我們這里可以通過alloc
的方式創建Buffer
我們可以直接對buffer實例以數組的形式對每一位進行修改
如果修改的是一個十進制數字,那它會自動幫助我們轉化成16進制的數字
如果修改的是一個十六進制數字,那么就會直接寫入
// 其可以指定我們buffer的位數,比如這里傳遞進去的是8,那么創建出來的buffer就有8個元素,且每個元素對應的二進制數都是0 const buffer = Buffer.alloc(8) console.log(buffer); // <Buffer 00 00 00 00 00 00 00 00> // 賦值為十進制數字的話,buffer會幫我們轉化為16進制數字再寫入到對應的位置 buffer[0] = 88 // 在js中,以0x開頭的就表示為16進制的數字 buffer[1] = 0x88 console.log(buffer); // <Buffer 58 88 00 00 00 00 00 00>
Buffer和文件操作
1、文本文件
如果未指定字符編碼,則不進行解碼,直接返回原始的 buffer
,也就是文件內容結果utf-8
編碼后的二進制數
const fs = require('fs') fs.readFile('./a.txt', (err, data) => { console.log(data); // <Buffer e5 93 88 e5 93 88> })
編碼和解碼用的都是utf-8,則可以得到文件中正確的內容
const fs = require('fs') // encoding表示解碼所用的字符編碼,編碼默認為utf-8 fs.readFile('./a.txt', { encoding: 'utf-8' }, (err, data) => { console.log(data); // 哈哈 })
如果編碼和解碼所用的字符編碼不同,則最終讀取出來的內容會亂碼
const fs = require('fs') // 編碼用的是utf16le字符編碼,解碼使用的是utf-8格式,肯定是解不是正確的內容的 fs.readFile('./a.txt', { encoding: 'utf16le' }, (err, data) => { console.log(data); // 鏥袓 }) // 以上代碼和下面代碼類似 const msg = '哈哈' const buffer = Buffer.from(msg, 'utf-8') console.log(buffer.toString('utf16le')); // 鏥袓
2、圖片文件
對圖片編碼進行拷貝,達到復制圖片的目的
讀取圖片的時候不要指定encoding
屬性,因為字符編碼只有在讀取文本文件的時候才有用
const fs = require('fs') fs.readFile('./logo.png', (err, data) => { console.log(data); // 打印出來的是圖片文件對應的二進制編碼 // 我們還可以將圖片編碼寫入到另一個文件當中,相當于我們將該圖片拷貝了一份 fs.writeFile('./bar.png', data, err => { console.log(err); }) })
對圖片進行翻轉、裁剪等操作,可以使用sharp
這個庫
const sharp = require('sharp') // 將logo.png這張圖片裁剪成200x300后拷貝到文件bax.png中 sharp('./logo.png') .resize(200, 300) .toFile('./bax.png', (err, info) => { console.log(err); }) // 還可以將圖片文件先轉為buffer,然后在寫入到文件中,也可以實現拷貝圖片的目的 sharp('./logo.png') .resize(300, 300) .toBuffer() .then(data => { fs.writeFile('./baa.png', data, err => { console.log(err); }) })
Buffer的創建過程
事實上我們創建Buffer
時,并不會頻繁的向操作系統申請內存,它會默認先申請一個8 * 1024
個字節大小的內存,也就是8kb
等到內存不夠或者快用完的時候才會去申請新的內存
什么是事件循環?
事件循環是什么?
事實上我把事件循環理解成我們編寫的JS
和瀏覽器或者Node
之間的一個橋梁
瀏覽器的事件循環是一個我們編寫的JS
代碼和瀏覽器API調用(setTimeout
、AJAX
、監聽事件
等)的一個橋梁,橋梁之間通過回調函數進行溝通
Node的事件循環是一個我們編寫的JS代碼和系統調用(file system
、networ
等)之間的一個橋梁,,橋梁之間也是通過回調函數進行溝通的
進程和線程
進程和線程是操作系統中的兩個概念:
進程(process
):計算機已經運行的程序
線程(thread
):操作系統能夠運行運算調度的最小單位,所以CPU
能夠直接操作線程
聽起來很抽象,我們直觀一點解釋:
進程:我們可以認為,啟動一個應用程序,就會默認啟動一個進程(也可能是多個進程)
線程:每一個進程中,都會啟動一個線程用來執行程序中的代碼,這個線程被稱之為主線程
所以我們也可以說進程是線程的容器
再用一個形象的例子解釋
操作系統類似于一個工廠
工廠中有很多車間,這個車間就是進程
每個車間可能有一個以上的工人在工廠,這個工人就是線程
多進程多線程開發
操作系統是如何做到同時讓多個進程(邊聽歌、邊寫代碼、邊查閱資料)同時工作呢?
這是因為CPU
的運算速度非常快,他可以快速的在多個進程之間迅速的切換
當我們的進程中的線程獲取到時間片時,就可以快速執行我們編寫的代碼
對于用戶來說是感受不到這種快速的切換的
瀏覽器和JavaScript
我們經常會說JavaScript
是單線程的,但是JS的線程應該有自己的容器進程:瀏覽器或者Node
瀏覽器是一個進程嗎,它里面只有一個線程嗎?
目前多數的瀏覽器其實都是多進程的,當我們打開一個tab
頁面時就會開啟一個新的進程,這是為了防止一個頁面卡死而造成所有頁面無法響應,整個瀏覽器需要強制退出
每個進程中又有很多的線程,其中包括執行JavaScript代碼的線程
但是JavaScript的代碼執行是在一個單獨的線程中執行的
這就意味著JS
的代碼,在同一時刻只能做一件事
如果這件事是非常耗時的,就以為這當前的線程就會被阻塞
JavaScript的執行過程
函數要被壓入函數調用棧中后才會被執行,下面我們來分析下代碼的執行過程
const message = 'Hello World' console.log(message); function sum(num1, num2) { return num1 + num2 } function foo() { const result = sum(20, 30) console.log(result); } foo()
我們JS的代碼其實也可以像其它編程語言一樣可以看成是在main
函數中執行的
那么首先我們要將main函數壓入函數調用棧中
定義變量message
執行log
函數,log函數會被放入到函數調用棧中,執行完后出棧
調用foo
函數,foo函數被壓入函數調用棧中,但是執行過程中又需要調用sum
函數
所以sum函數會被壓入到函數調用棧中,sum函數執行完畢后出棧
此時foo函數也得到了sum函數返回的值,并執行了賦值操作,但又遇到了log函數
所以又要將log函數壓入到調用棧,log函數被執行完畢,出棧后foo函數也執行完畢,foo函數出棧
foo函數執行完后,整個js
代碼執行完畢,main函數出棧
瀏覽器的事件循環
如果在執行JS
代碼的過程中,有異步操作呢?
比如中間我們插入了一個setTimeout
的函數調用
那么setTimeout這個函數被放入到調用棧中,執行會立即結束,并不會阻塞后續代碼的執行
那么,往setTimeout函數里面傳入的函數(我們稱之為timer
函數),會在什么時候被執行呢?
事實上,setTimeout是調用了web api
,瀏覽器會提前會將回調函數存儲起來,在合適的時機,會將timer函數加入到一個事件隊列中
事件隊列中的函數,會被放入到函數調用棧中,在調用棧中被執行
為什么setTimeout不會阻塞代碼的執行呢?就是因為瀏覽器里面維護了一個非常非常重要的東西——事件循環
瀏覽器中會通過某種方式幫助我們保存setTimeout中的回調函數的,比較常用的方法就是保存到一個紅黑樹里面
等到setTimeout定時器時間到達的時候,它就會將我們的timer回調函數從保存的地方取出來并放入到事件隊列里面
事件循環一旦發現我們的隊列中有東西了,并且當前函數調用棧是空的,其它同步代碼也執行完之后,就會將我們隊列中的回調函數依次出列放入到函數調用棧中執行(隊列中前一個函數出棧后,下一個函數才會入棧)
當然事件隊列中不一定只有一個事件,比如說在某個過程中用戶點擊了瀏覽器當中的某個按鈕,我們可能對這個按鈕的點擊做了一個監聽,對應了一個回調函數,那個回調函數也會被加入到我們的隊列里面的,執行順序按照它們在事件隊列中的順序執行。還有我們發送ajax
請求的回調,也是加入到事件隊列里面的
總結:其實事件循環是一個很簡單的東西,它就是在某一個特殊的情況下,需要去執行某一個回調的時候,它就把提前保存好的回調塞入事件隊列里面,事件循環再給它取出來放入到函數調用棧中
宏任務與微任務
但是事件循環中并非只維護一個隊列,事實上是有兩個隊列,而且隊列中的任務執行一定會等到所有的script都執行完畢后
宏任務隊列(macrotask queue
):ajax
、setTimeout
、setInterval
、DOM
監聽、UI Rendering
等
微任務隊列(microtask queue
):Promise
的then
回調、Mutation Observer API
、queueMicrotask()
等
那么事件循環對于兩個隊列的優先級是怎么樣的呢?
main script
中的代碼優先執行(編寫的頂層script代碼)
在執行任何一個宏任務之前(不是隊列,是一個宏任務),都會先查看微任務隊列中是否有任務需要執行
也就是宏任務執行之前,必須保證微任務隊列是空的
如果不為空,那么就優先執行微任務隊列中的任務(回調)
面試題<一>
考點:main stcipt
、setTimeout
、Promise
、then
、queueMicrotask
setTimeout(() => { console.log('set1');4 new Promise(resolve => { resolve() }).then(resolve => { new Promise(resolve => { resolve() }).then(() => { console.log('then4'); }) console.log('then2'); }) }) new Promise(resolve => { console.log('pr1'); resolve() }).then(() => { console.log('then1'); }) setTimeout(() => { console.log('set2'); }) console.log(2); queueMicrotask(() => { console.log('queueMicrotask'); }) new Promise(resolve => { resolve() }).then(() => { console.log('then3'); }) // pr1 // 2 // then1 // queueMicrotask // then3 // set1 // then2 // then4 // set2
setTimeout
會立即壓入函數調用棧,執行完畢后立即出棧,其timer
函數被放入到宏任務隊列中
傳入Promise
類的函數會被立即執行,其并不是回調函數,所以會打印出pr1
,并且由于執行了resolve
方法,所以該Promise的狀態會立即變為fulfilled
,這樣then
函數執行的時候其對應的回調函數就會被放入到微任務隊列中
又遇到了一個setTimeout函數,壓棧出棧,其timer函數會被放入到宏任務隊列中
遇到console.log
語句,函數壓棧后執行打印出了2
,然后出棧
這里通過queueMicrotask
綁定了個函數,該函數會被放入到微任務隊列中
又遇到了new Promise語句,但是其立即就將promise的狀態改為了fulfilled,所以then函數對應的回調也被放入到了微任務隊列中
由于同步腳本代碼已經執行完畢,現在事件循環開始要去把微任務隊列和宏任務對壘的任務按照優先級順序放入到函數調用棧中執行了,注意:微任務的優先級比宏任務高,每次想要執行宏任務之前都要看看微任務隊列里面是否為空,不為空則需要先執行微任務隊列的任務
第一個微任務是打印then1
,第二個微任務是打印queueMicrotask,第三個微任務是打印then3
,執行完畢后,就開始去執行宏任務
第一個宏任務比較復雜,首先會打印set1
,然后執行了一個立即變換狀態的new promise
語句,其then回調會被放入到微任務隊列中,注意現在微任務隊列可不是空的,所以需要執行優先級較高的微任務隊列,相當于該then回調被立即執行了,又是相同的new Promise語句,其對應的then對調被放入到微任務隊列中,注意new Promise語句后面還有一個console
函數,該函數會在執行完new Promise語句后立即執行,也就是打印then2
,現在微任務對壘還是有一項任務,所以接下來就是打印then4
。目前為止,微任務隊列已經為空了,可以繼續執行宏任務隊列了
所以接下里的宏任務set2
會被打印,宏任務執行完畢
整個代碼的打印結果是:pr1 -> 2 -> then1 -> queueMicrotask -> then3 -> set1 -> then2 -> then4 -> set2
面試題<二>
考點:main script
、setTimeout
、Promise
、then
、queueMicrotask
、await
、async
知識補充:async、await是Promise
的一個語法糖,在處理事件循環問題時
我們可以將await關鍵字后面執行的代碼,看做是包裹在new Promise((resolve,rejcet) => { 函數執行 })
中的代碼
await語句后面的代碼,可以看做是上一個Promise中的then(res => {函數執行})
中的代碼
async function async1() { console.log('async1 start'); await async2() console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0) async1() new Promise(resolve => { console.log('promise1'); resolve() }).then(() => { console.log('promise2'); }) console.log('script end'); // script start // async1 start // async2 // promise1 // script end // async1 end // promise2 // setTimeout
一開始都是函數的定義,不需要壓入函數調用棧中執行,直到遇到第一個console
語句,壓棧后執行打印script start
后出棧
遇到第一個setTimeout
函數,其對應的timer
會被放入到宏任務隊列中
async1函數被執行,首先打印出async1 start
,然后又去執行await
語句后面的async2
函數,因為前面也說了,將await關鍵字后面的函數看成是new Promise
里面的語句,這個函數是會被立即執行的,所以async2會被打印出來,但該await語句后面的代碼相當于是放入到then回調中的,也就是說console.log('async1 end')
這行代碼被放入到了微任務隊列里
代碼繼續執行,又遇到了一個new Promise語句,所以立即打印出了promise1
,then回調中的函數被放入到了微任務隊列里面去
最后一個console函數執行打印script end
,同步代碼也就執行完畢了,事件循環要去宏任務和微任務隊列里面執行任務了
首先是去微任務隊列,第一個微任務對應的打印語句會被執行,也就是說async1 end
會被打印,然后就是promise2
被打印,此時微任務隊列已經為空,開始去執行宏任務隊列中的任務了
timer函數對應的setTimeout會被打印,此時宏任務也執行完畢,最終的打印順序是:script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout
以上就是關于“Node.js中的Buffer和事件循環實例分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。