本篇文章給大家?guī)砹岁P(guān)于javascript的相關(guān)知識,其中主要整理了Promise的基本概念及使用方法的相關(guān)問題,包括了Promise基本概念、使用Promise解決回調(diào)地獄等等內(nèi)容,下面一起來看一下,希望對大家有幫助。
一、前言
異步是為了提高CPU的占用率,讓其始終處于忙碌狀態(tài)。
有些操作(最典型的就是I/O)本身不需要CPU參與,而且非常耗時,如果不使用異步就會形成阻塞狀態(tài),CPU空轉(zhuǎn),頁面卡死。
在異步環(huán)境下發(fā)生I/O操作,CPU就把I/O工作扔一邊(此時I/O由其他控制器接手,仍然在數(shù)據(jù)傳輸),然后處理下一個任務,等I/O操作完成后通知CPU(回調(diào)就是一種通知方式)回來干活。
《JavaScript異步與回調(diào)》想要表達的核心內(nèi)容是,異步工作的具體結(jié)束時間是不確定的,為了準確的在異步工作完成后進行后繼的處理,就需要向異步函數(shù)中傳入一個回調(diào),從而在完成工作后繼續(xù)下面的任務。
雖然回調(diào)可以非常簡單的實現(xiàn)異步,但是卻會由于多重嵌套形成回調(diào)地獄。避免回調(diào)地獄就需要解嵌套,將嵌套編程改為線性編程。
Promise
是JavaScript
中處理回調(diào)地獄最優(yōu)解法。
二、Promise基本概念
Promise
可以翻譯為“承諾”,我們可以通過把異步工作封裝稱一個Promise
,也就是做出一個承諾,承諾在異步工作結(jié)束后給出明確的信號!
Promise
語法:
let promise = new Promise(function(resolve,reject){ // 異步工作})
通過以上語法,我們就可以把異步工作封裝成一個Promise
。在創(chuàng)建Promise
時傳入的函數(shù)就是處理異步工作的方法,又被稱為executor
(執(zhí)行者)。
resolve
和reject
是由JavaScript
自身提供的回調(diào)函數(shù),當executor
執(zhí)行完了任務就可以調(diào)用:
resolve(result)
——如果成功完成,并返回結(jié)果result
;
reject(error)
——如果執(zhí)行是失敗并產(chǎn)生error
;
executor
會在Promise
創(chuàng)建完成后立即自動執(zhí)行,其執(zhí)行狀態(tài)會改變Promise
內(nèi)部屬性的狀態(tài):
state
——最初是pending
,然后在resolve
被調(diào)用后轉(zhuǎn)為fulfilled
,或者在reject
被調(diào)用時變?yōu)?code>rejected;
result
——最初時undefined
,然后在resolve(value)
被調(diào)用后變?yōu)?code>value,或者在reject
被調(diào)用后變?yōu)?code>error;
2.1 異步工作的封裝
文件模塊的fs.readFile
就是一個異步函數(shù),我們可以通過在executor
中執(zhí)行文件讀取操作,從而實現(xiàn)對異步工作的封裝。
以下代碼封裝了fs.readFile
函數(shù),并使用resolve(data)
處理成功結(jié)果,使用reject(err)
處理失敗的結(jié)果。
代碼如下:
let promise = new Promise((resolve, reject) => { fs.readFile('1.txt', (err, data) => { console.log('讀取1.txt') if (err) reject(err) resolve(data) }) })
如果我們執(zhí)行這段代碼,就會輸出“讀取1.txt”字樣,證明在創(chuàng)建Promise
后立刻就執(zhí)行了文件讀取操作。
Promise
內(nèi)部封裝的通常都是異步代碼,但是并不是只能封裝異步代碼。
2.2 Promise執(zhí)行結(jié)果獲取
以上Promise
案例封裝了讀取文件操作,當完成創(chuàng)建后就會立即讀取文件。如果想要獲取Promise
執(zhí)行的結(jié)果,就需要使用then
、catch
和finally
三個方法。
then
Promise
的then
方法可以用來處理Promise
執(zhí)行完成后的工作,它接收兩個回調(diào)參數(shù),語法如下:
promise.then(function(result),function(error))
第一個回調(diào)函數(shù)用于處理成功執(zhí)行后的結(jié)果,參數(shù)
result
就是resolve
接收的值;第二個回調(diào)函數(shù)用于處理失敗執(zhí)行后的結(jié)果,參數(shù)
error
就是reject
接收的參數(shù);
舉例:
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('成功執(zhí)行,結(jié)果是' + data.toString()) }, (err) => { console.log('執(zhí)行失敗,錯誤是' + err.message) })
如果文件讀取成功執(zhí)行,會調(diào)用第一個函數(shù):
PS E:\Code\Node\demos\03-callback> node .\index.js
讀取1.txt
成功執(zhí)行,結(jié)果是1
刪掉1.txt
,執(zhí)行失敗,就會調(diào)用第二個函數(shù):
PS E:\Code\Node\demos\03-callback> node .\index.js
讀取1.txt
執(zhí)行失敗,錯誤是ENOENT: no such file or directory, open 'E:\Code\Node\demos\03-callback\1.txt'
如果我們只關(guān)注成功執(zhí)行的結(jié)果,可以只傳入一個回調(diào)函數(shù):
promise.then((data)=>{ console.log('成功執(zhí)行,結(jié)果是' + data.toString()) })
到這里我們就是實現(xiàn)了一次文件的異步讀取操作。
catch
如果我們只關(guān)注失敗的結(jié)果,可以把第一個then
的回調(diào)傳null
:promise.then(null,(err)=>{...})
。
亦或者采用更優(yōu)雅的方式: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
.finally
是promise
不論結(jié)果如何都會執(zhí)行的函數(shù),和try...catch...
語法中的finally
用途一樣,都可以處理和結(jié)果無關(guān)的操作。
例如:
new Promise((resolve,reject)=>{ //something... }).finally(()=>{console.log('不論結(jié)果都要執(zhí)行')}).then(result=>{...}, err=>{...})
finally
回調(diào)沒有參數(shù),不論成功與否都會執(zhí)行
finally
會傳遞promise
的結(jié)果,所以在finally
后仍然可以.then
三、使用Promise解決回調(diào)地獄
3.1 回調(diào)地獄出現(xiàn)的場景
現(xiàn)在,我們有一個需求:使用fs.readFile()
方法順序讀取10個文件,并把十個文件的內(nèi)容順序輸出。
由于fs.readFile()
本身是異步的,我們必須使用回調(diào)嵌套的方式,代碼如下:
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()) // ==> 地獄之門 }) }) }) }) }) }) }) }) }) })
雖然以上代碼能夠完成任務,但是隨著調(diào)用嵌套的增加,代碼層次變得更深,維護難度也隨之增加,尤其是我們使用的是可能包含了很多循環(huán)和條件語句的真實代碼,而不是例子中簡單的 console.log(...)
。
3.2 不使用回調(diào)產(chǎn)生的后果
如果我們不使用回調(diào),直接把fs.readFile()
順序的按照如下代碼調(diào)用一遍,會發(fā)生什么呢?
//注意:這是錯誤的寫法 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()) })
以下是我測試的結(jié)果(每次執(zhí)行的結(jié)果都是不一樣的):
PS E:\Code\Node\demos\03-callback> node .\index.js12346957108
產(chǎn)生這種非順序結(jié)果的原因是異步,并非多線程并行,異步在單線程里就可以實現(xiàn)。
之所以在這里使用這個錯誤的案例,是為了強調(diào)異步的概念,如果不理解為什么會產(chǎn)生這種結(jié)果,一定要回頭補課了!
3.3 Promise解決方案
使用Promise
解決異步順序文件讀取的思路:
封裝一個文件讀取
promise1
,并使用resolve
返回結(jié)果使用
promise1.then
接收并輸出文件讀取結(jié)果在
promise1.then
中創(chuàng)建一個新的promise2
對象,并返回調(diào)用新的
promise2.then
接收并輸出讀取結(jié)果在
promise2.then
中創(chuàng)建一個新的promise3
對象,并返回調(diào)用新的
promise3.then
接收并輸出讀取結(jié)果…
代碼如下:
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()) //..... } ) ... ...
這樣我們就把原本嵌套的回調(diào)地獄寫成了線性模式。
但是代碼還存在一個問題,雖然代碼從管理上變的美麗了,但是大大增加了代碼的長度。
3.4 鏈式編程
以上代碼過于冗長,我們可以通過兩個步驟,降低代碼量:
封裝功能重復的代碼,完成文件讀取和輸出工作
省略中間
promise
的變量創(chuàng)建,將.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
,我們可以直接執(zhí)行.then
方法,這種編程方式被稱為鏈式編程。
代碼執(zhí)行結(jié)果如下:
PS E:\Code\Node\demos\03-callback> node .\index.js12345678910
這樣就完成了異步且順序的文件讀取操作。
注意:在每一步的
.then
方法中都必須返回一個新的Promise
對象,否則接收到的將是上一個舊的Promise
。這是因為每個
then
方法都會把它的Promise
繼續(xù)向下傳遞。