即使是 async / await 也是基於 Promise 的,任何的非同步過程我覺得都應該用 Promise 做抽象。
Promise 可以被理解為一種狀態機,或者函數語言程式設計裡的容器型別。
狀態機解釋
Promise 的抽象性源於它的命名:承諾。
Promise 是一臺蘊含著非同步過程以及結果的狀態機
Promise 的狀態機的狀態有且僅有這三種:
- Pendding 態
- Resolved 態
- Rejected 態
並且只有這兩種狀態轉換
- Pendding -> Resolved
- Pendding -> Rejected
既然是狀態機,那麼它肯定有輸出,它的輸出由其蘊含的非同步過程給出,用 then 可以將其挖出來。
具體如何理解, 先定義如下的 Promise:
let p = new Promise(function todo(resolve, reject){
console.log('waitting');
setTimeout(() => {
// 某些操作
resolve('ok');
}, 1000);
});
複製程式碼
new Promise 將會返回一個 Promise 例項,裡面包著一個非同步過程。
再來看看構造這個例項的時候用的引數吧:
function todo(resolve, reject){
console.log('waitting');
setTimeout(() => {
// 某些操作
resolve('ok');
}, 1000);
}
複製程式碼
對於這個函式的引數的描述是:
resolve 是函式,一旦被執行,這個 Promise 就會變成 Resolved 態 reject 是函式,一旦被執行,這個 Promise 就會變成 Rejected 態
因此,是這樣產生一個 Promise 例項的:
- 生成一個狀態為 Pendding 的 Promise 例項
- 取出上述例項的狀態轉化器 resolve 和 reject
- 應用到 todo 函式
一開始的時候 Promise 是 Pendding 態的,約 1000 毫秒後,resolve 被執行,p 變成 Resolved 態了。
一旦 Promise 的例項完成了狀態切換, 利用 then 方法就可以取得狀態切換的時候被傳遞的引數 ( 這裡是 'ok' )
p.then(ok => {
console.log(ok);
// =>
// 'ok'
});
複製程式碼
此外,then 方法本身也會返回一個 Promise :
var p2 = p.then(ok => {
console.log(ok);
// =>
// 'ok'
return 'ok from p2';
});
p2.then(ok => {
console.log(ok);
// =>
// 'ok from p2'
})
p.then(ok => {
console.log(ok);
// =>
// 依然是 'ok'
})
複製程式碼
這裡的 p2 由 p 生成,蘊含的值是 'ok from p2'。
而且,應當看到,一旦狀態確定 p 將永遠不變,因此無論怎麼搞,p 蘊含的值仍然還是 'ok'
而且既然 then 方法返回的是 Promise 那麼,很自然的,可以鏈式的使用 then :
let p3 = p.then(ok => {
console.log(ok);
// =>
// 'ok'
return new Promise(resolve => {
resolve('ok from a new promise')
})
}).then(ok => {
console.log(ok);
// =>
// 'ok from a new promise'
});
複製程式碼
注意 第四行 return 的值也可以是一個 Promise,之後的 then 將會如想象中的那樣是上一次 Promise 的蘊含結果了。
函式式解釋
Promise 的諸多表述都透露著函式式的氣息,比如 then 本身會返回一個新 Promise,而不是從舊的 Promise 上改變狀態而來,
此外,新的 Promise 還是舊 Promise 關於函式 F 的應用結果:
let p1 = new Promise(res => {
res('hello, star chan ~ ');
});
let F = str => str.toUpperCase();
let p2 = p1.then(F);
// just a simple mapping ?????
// let p2 = p1.map(F);
複製程式碼
上面的程式碼無非這樣:
p2 = F( p1 )
因而兩個 Promise 存在對映關係,新的 Promise 是舊 Promise 關於 F 的一個應用。
故此,Promise 是一類 Mappable 的 Container,是一種 Functor,因而函式式的某些重要的信條也有所體現:
- 狀態一經改變,則永遠不變,並且總是產生新的 Promise,而不是改變系統的狀態 (純的)
- 副作用的操作,被容器包在 F 裡了,觀測性更強 (可控性更高)
複製程式碼
可遞迴的一個例項
非同步的過程,一般而言,是很難遞迴的處理的,但是如果有 Promise,則可以同步寫非同步地來寫遞迴,好做的多。
現在有一個圖片列表,如果想要一個接著一個的非同步下載(不是一瞬間併發下載),遞迴實現如下:
// dlOneByOne.js
const rp = require('request-promise')
, fs = require('then-fs')
, url = require('url')
, path = require('path')
, STORE_TO = __dirname
, FILE_BASE = path.join(STORE_TO, 'd')
function download(raw_url){
const fileUrl = url.parse(raw_url)
, filePath = path.parse(fileUrl.path)
, fileName = filePath.base
, fileLocation = path.join(FILE_BASE, fileName)
, target = fs.createWriteStream(fileLocation)
console.log(fileLocation);
return new Promise((res, rej) => {
// Promise Resolved When Success
target.on('close', res);
// Promise Rejected When Error
target.on('error', rej);
// Start Downloading
rp.get(raw_url).pipe(target);
});
}
var dlOneByOne = ([x, ...xs]) => (
// x 存在嗎?
x ?
// 存在
download(x).then(download_success => {
return dlOneByOne(xs);
}) :
// 不存在
Promise.resolve('All Done')
);
dlOneByOne.download = download
module.exports = dlOneByOne;
複製程式碼
按照如下方法使用:
// test.js
const dlOneByOne = require('./dlOneByOne.js')
dlOneByOne([ /* 圖片url */ ]).then(ok => {
console.log('[ Succ ] All Done');
}).catch(err => {
console.log('[ Error ]', err);
})
複製程式碼
Promise.all & Array.prototype.map
字串陣列對映到 Promise 陣列最後摺合成一個 Promise
下面是一個併發下載全部檔案的例子
const { download } = require('./dlOneByOne.js');
Promise.all(
[ /* 圖片url */ ].map(download)
).then(all_dones => {
console.log('All Done');
});
複製程式碼
TL;DR
就... 沒有 Promise 根本沒法程式設計 :p