函数式的 Promise 对异步的抽象

1,131 阅读4分钟

即使是 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