2017-12-18
Promise
函数式的 Promise 对异步的抽象
即使是 async / await 也是基于 Promise 的,任何的异步过程我觉得都应该用 Promise 做抽象。
Promise 可以被理解为一种状态机,或者函数式编程里的容器类型。

# 状态机解释

Promise 的抽象性源于它的命名:承诺。
Promise 是一台蕴含着异步过程以及结果的状态机
Promise 的状态机的状态有且仅有这三种:
  1. Pendding 态
  2. Resolved 态
  3. Rejected 态
并且只有这两种状态转换
  1. Pendding -> Resolved
  2. Pendding -> Rejected
既然是状态机,那么它肯定有输出,它的输出由其蕴含的异步过程给出,用 then 可以将其挖出来。

具体如何理解, 先定义如下的 Promise:
00let p = new Promise(function todo(resolve, reject){
01    console.log('waitting'); 
02
03    setTimeout(() => {
04        // 某些操作 
05        resolve('ok'); 
06    }, 1000); 
07});
new Promise 将会返回一个 Promise 实例,里面包着一个异步过程。
再来看看构造这个实例的时候用的参数吧:
00function todo(resolve, reject){
01    console.log('waitting'); 
02
03    setTimeout(() => {
04        // 某些操作 
05        resolve('ok'); 
06    }, 1000); 
07}
对于这个函数的参数的描述是:
resolve 是函数,一旦被执行,这个 Promise 就会变成 Resolved 态
reject 是函数,一旦被执行,这个 Promise 就会变成 Rejected 态
因此,是这样产生一个 Promise 实例的:
  1. 生成一个状态为 Pendding 的 Promise 实例
  2. 取出上述实例的状态转化器 resolve 和 reject
  3. 应用到 todo 函数
一开始的时候 Promise 是 Pendding 态的,约 1000 毫秒后,resolve 被执行,p 变成 Resolved 态了。
一旦 Promise 的实例完成了状态切换, 利用 then 方法就可以取得状态切换的时候被传递的参数 ( 这里是 ‘ok’ )
00p.then(ok => {
01    console.log(ok); 
02    // => 
03    // 'ok'
04});
此外,then 方法本身也会返回一个 Promise :
00var p2 = p.then(ok => {
01    console.log(ok); 
02    // => 
03    // 'ok' 
04
05    return 'ok from p2'; 
06}); 
07
08p2.then(ok => {
09    console.log(ok); 
10    // => 
11    // 'ok from p2' 
12})
13
14p.then(ok => {
15    console.log(ok); 
16    // => 
17    // 依然是 'ok' 
18})
这里的 p2 由 p 生成,蕴含的值是 ‘ok from p2’。
而且,应当看到,一旦状态确定 p 将永远不变,因此无论怎么搞,p 蕴含的值仍然还是 ‘ok’
而且既然 then 方法返回的是 Promise 那么,很自然的,可以链式的使用 then :
00let p3 = p.then(ok => {
01    console.log(ok); 
02    // => 
03    // 'ok' 
04    return new Promise(resolve => {
05        resolve('ok from a new promise')
06    }) 
07}).then(ok => {
08    console.log(ok); 
09    // => 
10    // 'ok from a new promise' 
11});
注意 第四行 return 的值也可以是一个 Promise,之后的 then 将会如想象中的那样是上一次 Promise 的蕴含结果了。

# 函数式解释

Promise 的诸多行为都透露着函数式的气息,比如 then 本身会返回一个新 Promise,而不是从旧的 Promise 上改变状态而来,
此外,新的 Promise 还是旧 Promise 关于函数 F 的应用结果:
00let p1 = new Promise(res => {
01    res('hello, world ~ '); 
02}); 
03
04let F = str => str.toUpperCase(); 
05
06let p2 = p1.then(F); 
07// ps is promise of "HELLO, WORLD ~"
上面的代码无非这样:
p2 = F( p1 )
因而两个 Promise 存在映射关系,新的 Promise 是旧 Promise 关于 F 的一个应用。
故此,Promise 是一类 Mappable 的 Container,是一种 Functor,因而函数式的某些重要的信条也有所体现:
  1. 状态一经改变,则永远不变,并且总是产生新的 Promise,而不是改变系统的状态 (纯的)
  2. 副作用的操作,被容器包在 F 里了,观测性更强 (可控性更高)

# 可递归的一个实例

异步的过程,一般而言,是很难递归的处理的,但是如果有 Promise,则可以同步写异步地来写递归,好做的多。
现在有一个图片列表,如果想要一个接着一个的异步下载(不是一瞬间并发下载),递归实现如下:
00// dlOneByOne.js 
01const rp = require('request-promise')
02    , fs = require('then-fs')
03    , url = require('url')
04    , path = require('path')
05    , STORE_TO = __dirname
06    , FILE_BASE = path.join(STORE_TO, 'd')
07
08function download(raw_url){
09    const fileUrl = url.parse(raw_url)
10        , filePath = path.parse(fileUrl.path)
11        , fileName = filePath.base
12        , fileLocation = path.join(FILE_BASE, fileName)
13        , target = fs.createWriteStream(fileLocation)
14
15    console.log(fileLocation); 
16
17    return new Promise((res, rej) => {
18        // Promise Resolved When Success
19        target.on('close', res); 
20        // Promise Rejected When Error 
21        target.on('error', rej); 
22        // Start Downloading 
23        rp.get(raw_url).pipe(target);
24    }); 
25}
26
27var dlOneByOne = ([x, ...xs]) => (
28    // x 存在吗? 
29    x ?
30        // 存在 
31        download(x).then(download_success => {
32            return dlOneByOne(xs); 
33        }) :
34        // 不存在
35        Promise.resolve('All Done')
36); 
37
38dlOneByOne.download = download
39
40module.exports = dlOneByOne;
按照如下方法使用:
00// test.js 
01const dlOneByOne = require('./dlOneByOne.js')
02
03dlOneByOne([
04    'http://example.com/test/a.jpg',
05    'http://example.com/test/b.png',
06    'http://example.com/test/c.jpg',
07    'http://example.com/test/d.png',
08]).then(ok => {
09    console.log('[ Succ ] All Done'); 
10}).catch(err => {
11    console.log('[ Error ]', err); 
12})

# Promise.all & Array.prototype.map

字符串数组映射到 Promise 数组最后折合成一个 Promise
下面是一个并发下载全部文件的例子
00const { download } = require('./dlOneByOne.js'); 
01
02Promise.all(
03    [
04        'http://source.ijarvis.cn/blog/test/a.jpg',
05        'http://source.ijarvis.cn/blog/test/b.png',
06        'http://source.ijarvis.cn/blog/test/c.jpg',
07        'http://source.ijarvis.cn/blog/test/r.png',
08        'http://source.ijarvis.cn/blog/test/s.jpg',
09        'http://source.ijarvis.cn/blog/test/t.png',
10        'http://source.ijarvis.cn/blog/test/v.jpg',
11        'http://source.ijarvis.cn/blog/test/y.jpg',
12        'http://source.ijarvis.cn/blog/test/65912770_p2.png'
13    ].map(download)
14).then(all_dones => {
15    console.log('All Done'); 
16});

# TL;DR

就… 没有 Promise 根本没法编程 :p




回到顶部