分类「 ES6
2017-09-19
ES6
ES6 Promise
为了解决异步回调地狱的问题,社区提出了 Promise 的实现。
我曾经在 《异步发射器》 里写过如下代码
00a(A => {        // 异步操作 a 获得 A
01    b(B => {    // 异步操作 b 获得 B
02        c(C => {// 异步操作 c 获得 C
03            d(D => {
04                // 异步操作 d 获得 D
05                console.log(A, B, C, D); 
06            });
07        });
08    });
09});
这个回调嵌套很深 搞起来很让人不爽
而如果 a b c d 四个函数都是用 Promise 实现的话,以上代码可以简化成:
00Promise.all([
01    a(), 
02    b(), 
03    c(),
04    d()
05]).then((A, B, C, D) => {
06    console.log(A, B, C, D); 
07})
而且, abcd 四个过程是并发的,而不是一个接着一个。
Promise 是异步编程的一种解决方案,可以很大程度上减少缩进的深度

# 有限状态机

任意一个 Promise 实例都是一台状态机,它有三个状态:
  1. pending
  2. resolved
  3. rejected
其中 pending 是所有 Promise 实例的初始状态,而且只有两种状态转移:
  1. pending -> resolved
  2. pending -> rejected
一旦一个 Promise 实例发生状态转换,就意味着它只能是 resolved 和 rejected 而且不可逆转。
而且最重要的是, Promise 实例是摩尔型状态机,不是米勒型,这意味着一旦状态确定,其对外的值将不会改变。
而这个 对外的值 可以是一次 http 请求的结果,一次 IO 操作的结果等等。

# 理解 Promise

考虑到异步一般都有始终,而且结果又一般只有两种(成功和失败),基于以上两点, Promise 实例的状态便设计成 pending resolved 和 rejected 这三种情况了。
生成一个 Promise 实例需要一个函数接收两个状态转移器 (resolve 和 reject):
00var sayHello = new Promise((resolve, reject) => {
01    if (/* 异步操作成功 */){
02        resolve('成功了'); 
03    } else {
04        reject('失败了'); 
05    }
06})
一旦执行了 resolve 此 Promise 将会迁移到 resolved ,一旦执行了 reject 状态会迁移到 reject
至于 resolve 和 reject 所接收的参数,将会成为这个状态机的 输出
00var sayHello = new Promise((resolve, reject) => {
01    // 异步 
02    setTimeout(function(){
03        if (/* 异步操作成功 */){
04            resolve('成功了'); 
05        } else {
06            reject('失败了'); 
07        }
08    }, 500); 
09})
10
11sayHello.then(success => {
12    console.log(success); 
13}, err => {
14    console.log(err); 
15})
then 方法接收两个函数作为参数,而且这两个函数将会在状态迁移之后执行,如果迁移之后是 resolved 态,则执行第一个,否则执行第二个。
而且,此前说过 一旦状态确定,其对外的值将不会改变 ,对同一个 Promise 实例,只要状态确定了,不论调用多少次 then 方法,得到的结果都一样:
00for (let i = 0; i < 3; i++){
01    sayHello.then(success => {
02        console.log(success); 
03    }, err => {
04        console.log(err); 
05    })
06}
上述结果都执行 success 回调,结果均为 成功了
而且,then 方法本身会返回另外一个 Promise 实例:
00var sayHello2 = sayHello.then(success => {
01    console.log(success); 
02
03    return '成功了 2'; 
04})
05
06sayHello2.then(word => {
07    // 成功了2
08    console.log(word); 
09})
此外,这个新的 Promise 实例的状态为 resolved (即使原先的 Promise 是 rejected 的):
00var fail = new Promise((res, rej) => {
01    rej('失败了'); 
02}).then(console.log, err => {
03    // 失败了
04    console.log(err); 
05
06    return '失败了吗?'
07}).then(res => {
08    // 不走下面的 rej 而是走这里
09    console.log(res)
10}, rej => {
11    console.log('继续失败一次')
12})

# Promise.all

这是个静态方法,在本文一开始就讲过了,它接受一个 Promise 实例的数组并返回一个 Promise 实例,该实例在全部 Promise 都完成状态转换之后转变状态,而且,只有 Promise 数组内的全部实例都为 resolved 的时候,该 Promise 实例才会是 resolved,否则,将会变成 rejected :
00// 第一种情况 该实例转换为 resolved 
01var promises = [1, 2, 3, 4, 5].map(e => {
02    return new Promise((res, rej) => {
03        res(e * 2)
04    }); 
05}); 
06
07Promise.all(promises).then(arr => {
08    // [2, 4, 6, 8, 10]
09    console.log(arr); 
10})
00// 第二种情况,转换为 rejected
01var promises = [1, 2, 3, 4, 5].map(e => {
02    return new Promise((res, rej) => {
03        if (e !== 1){
04            res(e * 2)
05        } else {
06            rej('错误'); 
07        }
08    }); 
09}); 
10
11Promise.all(promises).then(arr => {
12    // [2, 4, 6, 8, 10]
13    console.log(arr); 
14}, rej => {
15    // 打印输出 '错误'
16    console.log(rej); 
17})

# Promise.race

与 all 方法类似接受一个 Promise 数组并返回一个 Promise 实例,但是如其名一样,只要这个数组里面的某一个最先改变状态成 X,则该实例将立即转换成 X
比如可以为 Promise 设置一个超时:
00Promise.race([
01    fetch('/resource-that-may-take-a-while'),
02    new Promise(function (resolve, reject) {
03        setTimeout(() => {
04            reject(new Error('request timeout'))
05        }, 5000)
06    })
07]).then(response => {
08    console.log(response); 
09}, err => {
10    console.log(err); 
11})

# Promise 链式使用

在此写一个读取文件夹下的所有 .md 文件并渲染成 html 输出到指定目录的函数:
00const fs = require('then-fs')
01    , dist = '/home/myblog'
02    , src = '/home/myblog-markdown'
03    , path = require('path')
04    // render 可以渲染 markdwon 字符串 
05    , render = require('./render')
06
07fs.readdir(src).then(files => {
08    // 文件夹下有 a.md 和 b.md 将其 map 成绝对路径:
09    // /home/myblog-markdown/a.md 和 /home/myblog-markdown/b.md 
10    return files.map(e => path.join(src, e))
11}).then(files => {
12    // 这里是绝对路径
13    let readAllFiles = files.map(file => {
14        return fs.readFile(file).then(data => {
15            let html = render(data.toString()); 
16            return {
17                html: html, 
18                filePath: file.replace('.md', '.html')
19            }
20        }); 
21    }); 
22
23    return Promise.all(readAllFiles)
24}).then(htmlDatas => {  
25    let saving = htmlDatas.map(data => {
26        let { html, filePath } = data; 
27
28        return fs.writeFile(filePath, html); 
29    });
30
31    return Promise.all(saving); 
32}).then(allDone => {
33    console.log('All Success')
34}).catch(err => {
35    console.log(err); 
36})
搭配链式调用,Promise 可以让编程者以同步的方式写异步,写异步的时候就不会很混乱了,可以完全过程化的写。
此外 Promise 还有很多有意思的方法,都可以在阮一峰的 ES6 书上查找学习,本文限于篇幅不再赘述。




回到顶部