
非同步是為了提高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繼續向下傳遞。