數據的二進位
電腦中所有的內容:文字、數字、圖片、音訊、影片最終都會使用二進位來表示
JS
可以直接去處理非常直覺的資料:例如字串,我們通常展示給使用者的也是這些內容
但你可能會以為JS也能夠處理圖片
JS
或者HTML
,只是負責告訴瀏覽器圖片的地址但是對於服務端來說是不一樣的
utf-8
進行編碼的,而是用GBK
,那麼我們必須讀取到他們的二進位數據,再透過GKB轉換成對應的文字sharp
的函式庫,就是負責讀取圖片或是傳入圖片的Buffer
對其再處理的Node
中透過TCP
建立長連接,TCP傳輸的是位元組流,我們需要將資料轉成位元組再傳入,並且需要知道傳輸位元組的大小(客戶端需要根據大小來判斷讀取多少內容)Buffer和二進位
我們會發現,對於前端開發來說,通常很少會和二進制打交道,但是對於伺服器端來說,為了實現很多功能,我們必須直接去操作其二進制的數據
所以Node
為了可以方便開發者完成更多功能,提供給了我們一個名為Buffer
的類,並且他是全局的
我們前面說過,Buffer中儲存的是二進位數據,那麼到底是如何儲存的呢?
8
位元二進位: 00000000
,剛好是一個位元組為什麼是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
實例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對我們的字串進行解碼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實例以數組的形式對每一位進行修改
// 其可以指定我們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> })
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
、监听事件
等)的一個橋樑,橋樑之間透過回呼函數進行溝通file system
、 networ
等)之間的一個橋樑,,橋樑之間也是透過回調函數進行溝通的行程和
執行緒進程和執行緒是作業系統中的兩個概念:
process
):電腦已經運行的程式執行thread
):作業系統能夠運行運算調度的最小單位,所以CPU
能夠直接操作執行緒聽起來很抽象,我們直觀一點解釋:
再用一個形象的例子解釋
多行程多執行緒開發
作業系統是如何做到同時讓多個行程(邊聽歌、邊寫程式碼、邊查閱資料)同時運作呢?
CPU
的運算速度非常快,他可以快速的在多個進程之間迅速的切換瀏覽器和JavaScript
我們常常會說JavaScript
是單執行緒的,但JS的執行緒應該有自己的容器程序:瀏覽器或是Node
瀏覽器是一個行程嗎,它裡面只有一個執行緒嗎?
tab
頁面時就會開啟一個新的進程,這是為了防止一個頁面卡死而造成所有頁面無法回應,整個瀏覽器需要強制退出但是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()
main
函數中執行的message
log
函數,log函數會被放入到函數呼叫堆疊中,執行完後出棧foo
函數,foo函數被壓入函數呼叫堆疊中,但是執行過程中又需要呼叫sum
函數js
程式碼執行完畢,main函數出棧瀏覽器的事件循環
如果在執行JS
程式碼的過程中,有非同步操作呢?
setTimeout
的函數呼叫那麼,往setTimeout函數裡面傳入的函數(我們稱之為timer
函數),會在什麼時候被執行呢?
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
在處理事件循環問題時
new Promise((resolve,rejcet) => { 函数执行})
中的程式碼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