let path = 'test.txt'
let data = readFile(path)
data = data + 'new word'
writeFile(path, data)
複製代碼
一般狀況下,咱們最但願程序能一步步按順序的執行,這和咱們的思惟方式一致。在最開始學習編程時,咱們也是先了解的結構化程序設計,經過順序結構、分支結構和循環結構組織代碼,這樣的代碼流程最清晰。node
但遺憾的是現實是殘酷的,文件操做、網絡操做等都是執行速度慢的操做。若是隻是順序執行,那麼在執行到這些地方時將會被卡住。大部分狀況下咱們只是想要這個操做的結果,等到須要用到數據時再取出使用。異步則是處理這個問題的方式。linux
將複雜耗時的過程封裝成可以當即從主流程返回,只在獲取到結果後通知主流程繼續處理的過程,就是異步化。調用者發起一個異步操做,而後當即返回去作別的事,異步操做經過狀態、回調等手段來通知調用者。因此異步函數一般有如下特色:ajax
nodejs的讀取文件操做fs.readFile(path[, options], callback)
是異步操做,在執行程序後當即返回,經過回調機制通知代碼獲取結果。算法
let fs = require("fs");
fs.readFile('input.txt', function (err, data) {
if (err) { return console.error(err); }
console.log(data.toString());
});
console.log('finish')
複製代碼
上面是一個最多見的node文件讀取方法,因爲讀取文件速度是慢操做,若是讀取文件是同步的,那麼後面的流程將會在這段時間卡住。給文件讀取添加一個回調,讓下面的流程不用等文件讀取結束先執行,等文件讀取完成後執行回調裏的任務,最後結束任務。這裏的文件讀取函數就是非阻塞的,文件讀取完成後執行的回調是異步操做。編程
nodejs也提供了同步文件操做readFileSync,最多見用在了依賴管理(CommonJS)的require中,這裏面讀取文件用的就是同步操做,可是node總體的風格仍是異步的。segmentfault
console.time('running')
let i = 10000000000
while(i>0) {
i--
}
console.timeEnd('running')
// running: 11448.334ms
複製代碼
在mac電腦打開活動監視器,運行後能夠看到啓動了一個node進程,且該進程的線程數量大於1,同時CPU利用率飆到了97.9%。api
能夠看到,程序啓動了一個node進程和多個線程。nodejs在設計上是單線程模型,雖然node啓動會建立多個線程(垃圾回收等),可是一個node進程只會啓動一個線程真正執行咱們寫的代碼。因此咱們說的nodejs是單線程指的不是指nodejs只會啓動一個線程。瀏覽器
瀏覽器打開一個tab也是啓動一個進程,進程下啓動多個線程,最終處理用戶js也是一個線程。網絡
在處理CPU密集型(圖片處理,加解密,複雜算法)等操做時,若是直接在代碼中運行有可能出現主流程暫時卡住狀況,有沒有辦法將他們轉換爲異步函數呢?多線程
先看看setTimeout,它自己是異步操做。在調用setTimeout後當即返回,且通過了設定時間後,通知主流程等待完成,只不過這個異步操做的執行內容只是等待,其餘什麼也沒作。setTimeout的異步並非咱們須要的能封裝耗時操做的異步,耗時操做仍是會在回調時同步地在主流程運行。
setTimeout(() => {
handlePicture()
}, 1000)
複製代碼
Promise則是一個包裝器,一般用它來處理回調嵌套的問題,他也只能將異步函數封裝成更易使用的異步函數,並不能將同步調用轉爲異步調用。在建立Promise後並非當即返回,而是同步執行裏面的回調函數。
new Promise((resolve) => {
handlePicture()
return resolve()
})
複製代碼
因此在語法層面並無可以提供同步轉異步的方法,很顯然若是須要轉爲異步操做,須要讓複雜操做不在主流程中處理,這就須要藉助其餘的工具來實現:
看到這裏你們有沒有想過,既然nodejs是單線程,那它是怎麼支持異步操做的?
let fs = require('fs');
let buffer = Buffer.alloc(1000000000, 1);
console.time('main')
console.time('write')
fs.writeFile('./data', buffer, (err, res) => {
console.timeEnd('write')
})
console.timeEnd('main')
// main: 0.661ms
// write: 1291.268ms
複製代碼
上面代碼執行後,因爲writeFile是異步操做,因此監視器能夠看到線程數量變多了,此時CPU利用率也不是很高。原來在執行到異步操做時,node會使用其餘線程來處理文件寫入操做,執行用戶代碼的線程繼續往下執行,等到異步操做執行完成後,再將結果交給用戶線程。接管和交回的動做是由事件循環機制實現。
nodejs是異步非阻塞模型。提到異步非阻塞,咱們會想到IOCP和AIO,只有這兩個是操做系統提供的最純潔的異步非阻塞模型。那爲何說nodejs也是異步非阻塞模型,跟這兩個方法有什麼關係嗎?
其實這只是站在不一樣維度得出的結論,咱們說的nodejs是站在應用層面說他是異步非阻塞,將文件,網絡調用等封裝成了異步非阻塞操做供應用層使用。最終writeFile在node接管後是由libuv實現任務分配調度。而libuv將同步轉爲異步過程則是由這個庫實現的多線程和操做系統I/O實現的,linux下時用的同步非阻塞epoll,window用的異步非阻塞IOCP。
既然都是異步調用了,確定是無法轉爲真同步的。那使用async/await不是能夠將代碼寫成同步的嗎?
let fs = require('fs');
(async() => {
let buffer = Buffer.alloc(1000000000, 1);
function myWriteFileSync() {
return new Promise((resolve) => {
fs.writeFile('./data', buffer, (err, res) => {
console.timeEnd('async')
return resolve()
})
})
}
function rawWriteFileSync() {
fs.writeFileSync('./data', buffer)
console.timeEnd('sync')
}
setInterval(() => {
console.log('interval...')
}, 500);
console.time('start')
console.time('end')
console.time('sync')
console.time('async')
console.timeEnd('start')
await myWriteFileSync()
rawWriteFileSync()
console.timeEnd('end')
})()
// start: 0.157ms
// interval...
// interval...
// async: 1455.406ms
// sync: 3048.463ms
// end: 3048.795ms
// interval...
// interval...
複製代碼
以上代碼咱們使用了原生的同步寫方法和使用async/await操做用Promise封裝的異步寫方法,在主線程中咱們使用interval每隔500ms打印一次。結果能夠看到在執行到await時,雖然沒有繼續往下執行,可是定時器任務仍是會觸發。可是若是是真正的同步操做,定時器也會被卡住不執行。因此說async/await只是個"假同步"。