非同步是為了提高CPU的佔用率,讓其始終處於忙碌狀態。
有些操作(最典型的就是I/O)本身不需要CPU參與,而且非常耗時,如果不使用非同步就會形成阻塞狀態,CPU空轉,頁面卡死。
在非同步環境下發生I/O操作,CPU就把I/O工作丟一邊(此時I/O由其他控制器接手,仍在資料傳輸),然後處理下一個任務,等I/O作業完成後通知CPU(回檔就是一種通知方式)回來工作。
《JavaScript非同步與回調》想要表達的核心內容是,非同步工作的具體結束時間是不確定的,為了準確的在非同步工作完成後進行後繼的處理,就需要向非同步函數中傳入一個回調,從而在完成工作後繼續下面的任務。
雖然回呼可以非常簡單的實作非同步,但卻會因為多重巢狀形成回調地獄。避免回調地獄就需要解嵌套,將嵌套編程改為線性編程。
Promise
是JavaScript
中處理回呼地獄最優解法。
Promise
可以翻譯為“承諾”,我們可以通過把異步工作封裝稱一個Promise
,也就是做出一個承諾,承諾在異步工作結束後給出明確的信號!
Promise
語法:
let promise = new Promise(function(resolve,reject){ // 非同步工作})
透過以上語法,我們就可以把非同步工作封裝成一個Promise
。在創建Promise
時傳入的函數就是處理非同步工作的方法,又稱為executor
(執行者)。
resolve
和reject
是由JavaScript
本身提供的回呼函數,當executor
執行完了任務就可以呼叫:
resolve(result)
-如果成功完成,並傳回結果result
;reject(error)
-如果執行是失敗並產生error
;executor
會在Promise
創建完成後立即自動執行,其執行狀態會改變Promise
內部屬性的狀態:
state
——最初是pending
,然後在resolve
被調用後轉為fulfilled
,或者在reject
被調用時變為rejected
;result
——最初時undefined
,然後在resolve(value)
被呼叫後變成value
,或是在reject
被呼叫後變成error
;檔模組的fs.readFile
就是一個非同步函數,我們可以透過在executor
中執行檔案讀取操作,從而實現對非同步工作的封裝。
下列程式碼封裝了fs.readFile
函數,並使用resolve(data)
處理成功結果,使用reject(err)
處理失敗的結果。
程式碼如下:
let promise = new Promise((resolve, reject) => { fs.readFile('1.txt', (err, data) => { console.log('讀取1.txt') if (err) reject(err) resolve(data) })})
如果我們執行這段程式碼,就會輸出「讀取1.txt」字樣,證明在創建Promise
後立刻就執行了檔案讀取操作。
Promise
內部封裝的通常都是非同步程式碼,但是並不是只能封裝非同步程式碼。
以上Promise
案例封裝了讀取檔案操作,完成建立後就會立即讀取檔案。如果想要取得Promise
執行的結果,就需要使用then
、 catch
和finally
三個方法。
Promise
的then
方法可以用來處理Promise
執行完成後的工作,它接收兩個回呼參數,語法如下:
promise.then(function(result),function(error))
result
就是resolve
接收的值;error
就是reject
接收的參數;範例:
let promise = new Promise((resolve, reject) => { fs.readFile('1.txt', (err, data) => { console.log('讀取1.txt') if (err) reject(err) resolve(data) })})promise.then( (data) => { console.log('成功執行,結果是' + data.toString()) }, (err) => { console.log('執行失敗,錯誤是' + err.message) })
如果檔案讀取成功執行,會呼叫第一個函數:
PS E:CodeNodedemos 3-callback> node .index.js 讀取1.txt 成功執行,結果是1
刪掉1.txt
,執行失敗,就會呼叫第二個函數:
PS E:CodeNodedemos 3-callback> node .index.js 讀取1.txt 執行失敗,錯誤是ENOENT: no such file or directory, open 'E:CodeNodedemos 3-callback1.txt'
如果我們只專注於成功執行的結果,可以只傳入一個回呼函數:
promise .then((data)=>{ console.log('成功執行,結果是' + data.toString())})
到這裡我們就是實作了一次檔案的非同步讀取操作。
如果我們只專注在失敗的結果,可以把第一個then
的回呼傳null
: promise.then(null,(err)=>{...})
。
也或採用更優雅的方式: promise.catch((err)=>{...})
let promise = new Promise((resolve, reject) => { fs.readFile('1.txt', (err, data) => { console.log('讀取1.txt') if (err) reject(err) resolve(data) })})promise.catch((err)=>{ console.log(err.message)})
.catch((err)=>{...})
和then(null,(err)=>{...})
作用完全相同。
.finally
是promise
不論結果如何都會執行的函數,和try...catch...
語法中的finally
用途一樣,都可以處理和結果無關的操作。
例如:
new Promise((resolve,reject)=>{ //something...}).finally(()=>{console.log('不論結果都要執行')}).then(result=>{...}, err=>{...} )
finally
回呼沒有參數,無論成功與否都會執行finally
會傳遞promise
的結果,所以在finally
後仍然可以.then
現在,我們有一個需求:使用fs.readFile()
方法順序讀取10個文件,並把十個文件的內容順序輸出。
由於fs.readFile()
本身是非同步的,我們必須使用回呼嵌套的方式,程式碼如下:
fs.readFile('1.txt', (err, data) => { console.log(data.toString()) //1 fs.readFile('2.txt', (err, data) => { console.log(data.toString()) fs.readFile('3.txt', (err, data) => { console.log(data.toString()) fs.readFile('4.txt', (err, data) => { console.log(data.toString()) fs.readFile('5.txt', (err, data) => { console.log(data.toString()) fs.readFile('6.txt', (err, data) => { console.log(data.toString()) fs.readFile('7.txt', (err, data) => { console.log(data.toString()) fs.readFile('8.txt', (err, data) => { console.log(data.toString()) fs.readFile('9.txt', (err, data) => { console.log(data.toString()) fs.readFile('10.txt', (err, data) => { console.log(data.toString()) // ==> 地獄之門}) }) }) }) }) }) }) }) })})
雖然以上程式碼能夠完成任務,但是隨著呼叫嵌套的增加,程式碼層次變得更深,維護難度也隨之增加,尤其是我們使用的是可能包含了很多循環和條件語句的真實程式碼,而不是例子中簡單的console.log(...)
。
如果我們不使用回調,直接把fs.readFile()
順序的依照以下程式碼呼叫一遍,會發生什麼事呢?
//注意:這是錯誤的寫法fs.readFile('1.txt', (err, data) => { console.log(data.toString())})fs.readFile('2.txt', (err, data) => { console.log(data.toString())})fs.readFile('3.txt', (err, data) => { console.log(data.toString())})fs.readFile('4.txt', (err, data) => { console.log(data.toString())})fs.readFile('5.txt', (err, data) => { console.log(data.toString())})fs.readFile('6.txt', (err, data) => { console.log(data.toString())})fs.readFile('7.txt', (err, data) => { console.log(data.toString())})fs.readFile('8.txt', (err, data) => { console.log(data.toString())})fs.readFile('9.txt', (err, data) => { console.log(data.toString())})fs.readFile('10.txt', (err, data) => { console.log(data.toString())})
以下是我測試的結果(每次執行的結果都是不一樣的):
PS E:CodeNodedemos 3-callback> node .index. js12346957108
產生這種非順序結果的原因是異步,並非多執行緒並行,非同步在單執行緒就可以實現。
之所以在這裡使用這個錯誤的案例,是為了強調非同步的概念,如果不理解為什麼會產生這種結果,一定要回頭補課了!
使用Promise
解決非同步順序檔案讀取的想法:
promise1
,並使用resolve
返回結果promise1.then
接收並輸出檔案讀取結果promise1.then
中建立一個新的promise2
對象,promise2.then
接收並promise2.then
中建立一個新的promise3
對象,並promise3.then
接收並輸出讀取結果程式碼如下:
let promise1 = new Promise( (resolve, reject) => { fs.readFile('1.txt', (err, data) => { if (err) reject(err) resolve(data) })})let promise2 = promise1.then( data => { console.log(data.toString()) return new Promise((resolve, reject) => { fs.readFile('2.txt', (err, data) => { if (err) reject(err) resolve(data) }) }) })let promise3 = promise2.then( data => { console.log(data.toString()) return new Promise((resolve, reject) => { fs.readFile('3.txt', (err, data) => { if (err) reject(err) resolve(data) }) }) })let promise4 = promise3.then( data => { console.log(data.toString()) //..... })... ...
這樣我們就把原本嵌套的回呼地獄寫成了線性模式。
但是程式碼還存在一個問題,雖然程式碼從管理上變的美麗了,但是大大增加了程式碼的長度。
程式設計以上程式碼過於冗長,我們可以透過兩個步驟,降低程式碼量:
promise
的變數創建,將.then
連結起來如下:
function myReadFile (path) { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err) console.log(data.toString()) resolve() }) })}myReadFile('1.txt') .then(data => { return myReadFile('2.txt') }) .then(data => { return myReadFile('3.txt') }) .then(data => { return myReadFile('4.txt') }) .then(data => { return myReadFile('5.txt') }) .then(data => { return myReadFile('6.txt') }) .then(data => { return myReadFile('7.txt') }) .then(data => { return myReadFile('8.txt') }) .then(data => { return myReadFile('9.txt') }) .then(data => { return myReadFile('10.txt') })
由於myReadFile
方法會傳回一個新的Promise
,我們可以直接執行.then
方法,這種程式設計方式稱為鍊式程式設計。
程式碼執行結果如下:
PS E:CodeNodedemos 3-callback> node .index.js12345678910
這樣就完成了非同步且順序的檔案讀取操作。
注意:在每一步的
.then
方法中都必須返回一個新的Promise
對象,否則接收到的將是上一個舊的Promise
。這是因為每個
then
方法都會把它的Promise
繼續向下傳遞。