最近一段時間參與開發了一個Node.js後臺項目,做爲一個PHP開發者,上手項目自己並不難,可是開發的過程卻並不順利,不順利的主要緣由在於思路上沒有轉變,沒有從同步
的思惟轉換到異步
的思惟。html
所謂同步
,就是程序(進程/線程)在一個任務的處理過程當中,不會插入處理其餘任務,即便遇到IO等不佔CPU的操做,也會一直等待其結束纔會繼續往下處理。node
所謂異步
,就是程序(進程/線程)在一個任務的處理過程當中,會插入處理其餘任務,如遇到IO操做,當前任務會將程序(進程/線程)的控制權釋放給其餘任務,等IO操做結果返回後再繼續往下處理。程序員
簡單地講,同步不會釋放控制權,異步會釋放控制權。編程
衆所周知,Node.js採用的是單線程的異步模型,在具體代碼的寫法上天然和PHP等同步模型不同。在具體項目開發的過程當中,各類異步操做相關的關鍵字層出不窮,如:.then()
、function* ... yield
、async...await
等等。爲了寫一個相似同步的操做,好比:「在執行完A步驟拿到結果以後再執行B步驟」這麼一個簡單的需求,卻要通過大量的反覆調試驗證才能解決。究其緣由,就是對於這些異步操做的場景和關鍵字的含義理解不到位,異步操做所提供的選擇太多了。api
下面就結合代碼實例,理一理這些異步操做方案究竟怎麼使用。promise
任務說明:項目根目錄下有三個文件Jay.txt
、Angela.txt
、Henry.txt
,依次讀取這三個文件的內容並打印。異步
下面使用各類異步處理的方法來完成此任務。async
const fs = require('fs'); fs.readFile('Jay.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); fs.readFile('Angela.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); fs.readFile('Henry.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); }); }); console.log("finish");
一、函數 fs.readFile() 用於異步讀取文件的全部內容,該函數自己沒有返回值。讀取的文件內容異步返回後經過回調函數處理。
二、函數fs.readFile()
的第二個參數是可選參數,若是指定了編碼方式,則返回對應編碼方式的字符串;若是沒有指定,則返回文件的二進制內容,對應類型爲Buffer,能夠經過 buf.toString() 方法轉換成對應的字符串。
三、回調函數的第一個參數必須是 錯誤對象,若是沒有錯誤則錯誤對象的值爲null
。
執行程序:函數
$ node 0A_callback_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry.
程序最早返回finish
,是由於函數fs.readFile()
是異步處理的,在調用後會直接繼續往下處理,在文件內容返回後經過註冊的回調函數處理。學習
串行和並行
一般,人們老是分不清同步
、異步
、串行
、並行
以前的區別,樸素地認爲:同步就是串行,異步就是並行
。這麼講彷佛對又彷佛不對。
同步
和異步
是從程序(進程/線程)執行方式的角度來看的,文章開頭已經簡單講過同步
和異步
的概念和區別。若是在程序的執行的過程當中不發生任務切換,即:作當前任務的一件事情,等待這件事情完成後,再作當前任務的下一件事情,直到當前任務完成,這種方式就是同步
。若是在程序的執行的過程當中發生任務切換,即:作當前任務的一件事情,不等待這件事情作完,直接轉去作其餘任務,再作當前任務的下一件事情,如此往復,直到當前任務完成,這種方式就是異步
。
串行
和並行
是從任務(事情)的角度來看的,若是多個任務(事情)不能同時作,而是作完一個才能作下一個,則就將這幾個任務(事情)稱做是串行
的。若是多個任務(事情)能夠同時作,則就將這幾個任務(事情)稱做是並行
的。
同步就是串行
這句話在必定程度上是正確
的,由於同步程序作完一件事情,纔會作下一件事情,從兩件事情上看,是不會同時作的,因此同步程序只能串行地作事情。
異步就是並行
這句話就不是那麼回事了,異步程序能夠選擇串行地作事情,也能夠選擇並行地作事情,是串行作仍是並行作取決於具體的業務場景。對於一個任務下的兩件事情A和B,若是B依賴於A的結果,則須要串行;若是B不依賴於A的結果,則能夠並行。仍是以本文的讀取三個文件爲例,上面的代碼示例就是串行
執行的,依次讀取"Jay.txt"
、"Angela.txt"
、"Henry.txt"
的內容並打印出來。若是要改爲並行執行該怎麼作呢?簡單作個改造就行,以下。
const fs = require('fs'); fs.readFile('Jay.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); fs.readFile('Angela.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); fs.readFile('Henry.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); console.log("finish");
執行程序:
finish Hello, I'm Angela. Hello, I'm Jay. Hello, I'm Henry.
從結果也能夠看出,因爲三個文件是並行讀取的,因此哪一個先讀完是隨機
的,和代碼寫的順序無關。按順序寫的代碼就會按順序執行
,這是典型的同步編程思惟
,要儘快轉變過來,不然遲早有一天會「翻車」的。
Promise對象可以表示一個異步操做的狀態和結果,使用其提供的.then()
方法能夠將多個多個異步操做「串聯」起來,.then()
方法自己也返回一個Promise對象。
一樣是按順序讀取三個文件的任務,示例以下:
var readFilePromise = require('fs-readfile-promise'); readFilePromise('Jay.txt', 'utf8') .then(function(data) { console.log(data); }) .then(function() { return readFilePromise('Angela.txt', 'utf8'); }) .then(function(data) { console.log(data); }) .then(function() { return readFilePromise('Henry.txt', 'utf8'); }) .then(function(data) { console.log(data); }) .catch(function(err) { console.log(err); }); console.log("finish");
執行程序:
$ node 0B_promise_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry.
Promise對象有pending(初始)
、fulfilled(成功)
、rejected(失敗)
三種狀態。當異步操做成功時,Promise對象從pending狀態變爲fulfilled狀態,並將成功結果傳遞給.then()
方法的第一個參數(也叫onfulfilled
函數);當異步操做失敗時,Promise對象從pending狀態變爲rejected狀態,並將失敗信息傳遞給.then()
方法的第二個參數(也叫onrejected
函數),若是沒有指定第二個參數,則將失敗信息傳遞給.catch()
方法的參數(一樣也叫onrejected
函數)。
上面的程序,能夠將上個文件的處理和下個文件的讀取合併到一個.then()
當中,示例以下:
var readFilePromise = require('fs-readfile-promise'); readFilePromise('Jay.txt', 'utf8') .then(function(data) { console.log(data); return readFilePromise('Angela.txt', 'utf8'); }) .then(function(data) { console.log(data); return readFilePromise('Henry.txt', 'utf8'); }) .then(function(data) { console.log(data); }) .catch(function(err) { console.log(err); }); console.log("finish");
.then()
當中,能夠返回一個Promise對象,能夠返回一個基礎類型的值(數字、字符串、布爾值),也能夠什麼都不返回(直接return;
),甚至連return
語句均可以省略。這幾種場景下的處理方式參考:.then()
方法的 返回值說明。
Generator函數
(生成器函數)使用 function* 關鍵字定義,函數中使用yield
關鍵字進行流程控制,yield
後面能夠跟任何表達式(普通同步表達式、Promise對象、Generator函數)。須要特別注意的是,yield
關鍵字必須放在Generator函數當中,不然運行時會報錯!
Generator函數
的返回值叫作 Generator對象(生成器對象),Generator對象有一個 .next() 方法,每執行一次.next()
方法,就會迭代執行至Generator函數
的下一個yield
語句位置,並返回一個對象。該對象包含兩個屬性:value
和done
,value
存儲了yield
後面表達式的值;done
是一個布爾值,表示Generator函數是否執行完畢。
一樣是按順序讀取三個文件的任務,示例以下:
var readFilePromise = require('fs-readfile-promise'); function* generator() { yield readFilePromise('Jay.txt', 'utf8'); yield readFilePromise('Angela.txt', 'utf8'); yield readFilePromise('Henry.txt', 'utf8'); } let gen = generator(); gen.next().value.then(function(data) { console.log(data); gen.next().value.then(function(data) { console.log(data); gen.next().value.then(function(data) { console.log(data); gen.next(); // 返回:{ value: undefined, done: true },表示生成器函數執行結束 }); }); }); console.log("finish");
執行程序:
$ node 0C_generator_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry.
本例中,
generator()
是一個生成器函數,其返回值gen
是一個生成器對象。gen.next()
返回的對象結構以下。{ value: Promise { <pending> }, done: false }其中,
gen.next().value
是一個Promise,表示yield
後面的readFilePromise()
函數所返回的是一個Promise對象。
須要注意的是,生成器函數自己包含的各個異步操做並不能按照順序串行執行,想要實現串行執行的話,仍是須要配合Promise對象及其.then()
函數來實現,如本例所示。
co
函數庫是幹什麼的?co
函數庫是Generator函數的一種執行器
。簡單來說,co
函數庫用來將上一節中手動執行Generator函數的過程自動化
,這樣一來,就使得採用同步思惟寫異步代碼
的想法成爲現實。做爲曾經是徹底同步思惟
的程序員終於看到了曙光。
一樣是按順序讀取三個文件的任務,示例以下:
var co = require('co'); var readFilePromise = require('fs-readfile-promise'); // generator()是一個生成器函數 function* generator() { let data = yield readFilePromise('Jay.txt', 'utf8'); console.log(data); data = yield readFilePromise('Angela.txt', 'utf8'); console.log(data); data = yield readFilePromise('Henry.txt', 'utf8'); console.log(data); } let gen = generator(); // gen是一個生成器對象 co(generator()).then(function() { console.log('Generator function is finished!'); }); console.log("finish");
執行程序:
$ node 0D_co_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry. Generator function is finished!
固然,用好co
的前提是有一些注意事項須要知道的:
一、co
函數配套使用的Generator函數
中,yield
後面的異步操做須要返回一個Promise對象,不然就沒法實現指望的同步
效果;
二、co
函數自己會返回一個Promise對象,因此如本例所示,是能夠在其後使用.then()
方法增長回調函數的。
co
庫函數已經將Generator
函數的執行簡化了不少,還能更簡單一點嗎?答案是:有,那就是async
函數。
async
函數與Generator
函數相比,能夠簡單地理解爲:將Generator函數中的*改成async,將yield改成await,就成了async函數
。
async
函數與Generator
函數相比:
async
函數自己內置了執行器,無需再像Generator
函數同樣須要引入額外的執行器(如:co
執行器);async...await
與function*...yield
相比,語義更加清晰明瞭:async
表示函數中有異步操做,await
表示須要等待異步操做返回結果;await
後面除了能夠跟Promise對象以外,也能夠跟基礎類型的值,如:數字、字符串、布爾值,而yield
後面必需要跟Promise對象;async
函數的返回值也是一樣是按順序讀取三個文件的任務,示例以下:
var readFilePromise = require('fs-readfile-promise'); async function asyncReadFile() { let data = await readFilePromise('Jay.txt', 'utf8'); console.log(data); data = await readFilePromise('Angela.txt', 'utf8'); console.log(data); data = await readFilePromise('Henry.txt', 'utf8'); console.log(data); return "Async function is finished!" } asyncReadFile().then(function(data) { console.log(data); }); console.log("finish");
執行程序:
$ node 0E_async_await_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry. Async function is finished!
從本例能夠看出,除了async
和await
這兩個關鍵字以外,總體代碼的格式與函數調用方式和同步代碼徹底同樣。因而可知,異步
代碼寫法的終極目標就是讓異步代碼寫起來和同步代碼同樣簡單方便
。然而,悲催的是,做爲一個JS新人須要花很久才能把這些逐漸弄清楚,學習成本不可謂不高。