为了解决异步回调地狱的问题,社区提出了 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 实例都是一台状态机,它有三个状态:
- pending
- resolved
- rejected
其中 pending 是所有 Promise 实例的初始状态,而且只有两种状态转移:
- pending -> resolved
- 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 书上查找学习,本文限于篇幅不再赘述。