函式式的 Promise 對非同步的抽象

eczn發表於2018-02-04

即使是 async / await 也是基於 Promise 的,任何的非同步過程我覺得都應該用 Promise 做抽象。

Promise 可以被理解為一種狀態機,或者函數語言程式設計裡的容器型別。

狀態機解釋

Promise 的抽象性源於它的命名:承諾。

Promise 是一臺蘊含著非同步過程以及結果的狀態機

Promise 的狀態機的狀態有且僅有這三種:

  1. Pendding 態
  2. Resolved 態
  3. Rejected 態

並且只有這兩種狀態轉換

  1. Pendding -> Resolved
  2. 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 例項的:

  1. 生成一個狀態為 Pendding 的 Promise 例項
  2. 取出上述例項的狀態轉化器 resolve 和 reject
  3. 應用到 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,因而函式式的某些重要的信條也有所體現:

  1. 狀態一經改變,則永遠不變,並且總是產生新的 Promise,而不是改變系統的狀態 (純的)
  2. 副作用的操作,被容器包在 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

相關文章