Promise
A promise represents the eventual result of an asynchronous operation.
本文可能不太适合初次接触 Promise 的人,你可以先大致了解一下 Promise 再来看,可以参考 promisejs.org 的介绍
基础介绍
Promise 把异步处理对象和处理规则规范化,统一接口编写, 模块化异步操作 ,这使得 Promise 异常的方便与强大。
优点:
- 模式化的操作使得异步的操作以同步的形式表达,避免了回调地狱
- 更加方便的异步操作
- 方便的异常处理
特点:
- 状态不受外界影响
- 状态改变之后不会在变
缺点:
- 一旦启动,不可终止
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
- 当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
本文主要介绍 ES6 Promise
经常看到的 Promise/A+ 是 ES6 Promises的前身,是一个社区规范,它和 ES6 Promises 有很多共通的内容。
Promise 概览
Promise 有三种状态
- pending: initial state, not fulfilled or rejected.
- fulfilled: meaning that the operation completed successfully.
- rejected: meaning that the operation failed.
备注
- 后两者状态为 settled 状态
- resolved 状态指 promise 变成 settled 状态或者 it has been “locked in” to match the state of another promise
很多博客并不区分 resolved 与 fulfilled,这是不太正确的
误解原因可能是因为在构造函数中的参数为
fn(resolve, reject)
,而在then中参数为.then(function onFulfilled(value), function onRejected(error))
基本流程如下:
各个细节 :
new Promise
- 构造函数 : 接受一个接受两个函数参数(resolve, reject) 的函数
- resolve: Pending => Resolved,将结果传入下一步
- reject : Pending => Rejected,将结果传入下一步
var promise = new Promise(function (resolve, reject) {
// 异步操作
if (/* 操作成功 */) {
resolve(result);
} else {
reject(error);
}
})
注意
- 一旦创建,立即执行
- 一定是异步操作,即使是在 Promise 进行同步操作
- 会比
setTimeout
中的时延函数更早执行,这是由于一般 Promise 的实现会使用更加迅速的异步执行操作(setTimeout至少有 4ms 时延)
例如 nodeJs 的process.nextTick()
- 为什么同步也要异步执行呢?
为了保证执行顺序的稳定: 如果在 Promise 中某些操作可能是同步也可能是异步,执行顺序将不确定
- 会比
- reject 参数: 无限制,建议是 Error
- resolve 和 reject 参数都可接受一个 thenable 对象(一个有 then 方法的对象),转化方法可参考之后的
[[Resolve]](promise, x)
- 返回值没什么用
then
promise2 = promise1.then(onFulfilled, onRejected);
- 接受两个回调函数
- 第一个函数为 状态变为 Fulfilled 时调用,参数为 resolve 函数处理得到
- 第二个函数为 状态变为 Rejected 时调用,参数为 reject 函数传入的值
- 这两个函数最多执行一次
- 两个函数都可省略(或者不是函数,会被忽略) : 省略的话,会值穿透,即会将应该处理的 value 传递给下一个 then 函数
注意:
- then 的返回是一个新 Promise 实例, 所以可以链式调用
- 回调函数 return 的值(x)会由
[[Resolve]](promise2, x).
包装处理 - 函数中抛出的错误(无论是被动或者是主动使用
throw
) 均会使 promise2 以 reason 被 rejected - 主动进入 rejected 状态的方法可以是 throw 抛出错误或者返回一个 rejected 的Promise
catch
- 是
.then(null, onRejected)
的别名 - 因为其实就是 then,所以具体细节同 then
- 在
resolved
之后抛出错误无效 - 记得之前的值穿透吧 : 错误会一直传递到捕获为止
- 说这一点是因为有时候链式使用 then 不写相应的处理错误方法(catch 或 第二个 then 参数)
- 之所以强调值穿透,是因为有时候会遇到第一个函数省略的情况,这种情况少见,但是一旦出现,容易迷惑
为什么要使用 catch
呢?
- 因为 catch 可以捕获之前 then 中 fulfill 调用发生的错误
- 这一点其实很强大,比如异步(网络,文件等)获得一个 json 文件,然后在 fulfill 函数中调用
JSON.parse()
是有可能错误的 - 如果你忘了写对应的 try-catch 块,错误发生的时候, 下一级的 catch 可以帮助捕获这个错误
- 注意,then 中的 onRejected 函数总是与 onFulfilled 函数同级,而 catch 是下一级,所以 catch 块更适合去处理错误
- 这一点其实很强大,比如异步(网络,文件等)获得一个 json 文件,然后在 fulfill 函数中调用
- 这也是一把双刃剑
- 因为 catch 会捕获错误,比如如果错误是一些比较奇怪的错误,比如因为你拼写错误导致的错误也会被捕捉
- 不用 catch, 那么错误会去哪呢?
- 会被 Promise 捕捉,catch 只是用来处理传递过来的错误的,并没有捕捉能力
- 这就意味着 then, catch 块总可能有错误被 Promise 捕捉,而不报错
- 但其实许多 Promise 库在有未处理的错误时会有提示,可以在 Chrome 下试试
- 非 ES6 Promise 标准的
.done
方法总是可以向上抛出错误(现在可以想想怎么做,稍后来讲)
注意 ECMAScript3 保留字无法作为对象属性名使用, 因此不能使用 Promise.catch() 的代码
需要使用中括号表示法或者某些类库使用了 caught 作为函数名
The Promise Resolution Procedure
[[Resolve]](promise, x)
解析过程如下(翻译自 Promises/A+):
- promise 与 x 指向同一对象, 以 TypeError 错误进入 rejected
- x 是 Promise
- x 处于 pending, promise 也保持 pending 直到 x 进入 fulfilled 或 rejected 状态
- x fulfilled 或者 rejected, 使 promise 也进入相同的状态并拥有相同的值
- x 是对象或者函数
- 定义 then 为 x.then
- 尝试 retrieve x.then,如果出错,抛出 Error,则使promise 以 Error 进入rejected
- 如果 x 是函数,则设 x 为 this 调用 x, 参数为 resolve 和 reject
- 如果 resolve 调用,参数为 y, 则运行
[[Resolve]](promise, y)
- 如果 reject 调用,参数为 r, 则 promise 因 r 而 reject
- 如果 resolve, reject 都调用或者多次调用, 取第一次调用,其他被忽略
- 调用 then 抛出异常:
如果 resolve, reject 已被调用,则忽略
否则 reject Promise 以异常 e
- 如果 resolve 调用,参数为 y, 则运行
- then 不是函数,promise 以 x 进入 fulfilled
- x 不是对象或者函数,promise 以 x 进入 fulfilled
这里要特别强调的是,非常推荐看一次 Promises/A+ 规范,正是因为 thenable 有着规范,所以 promise 可以在不同的类库之间相互转换
thenable 指有 then 方法的对象
Promise.resolve()
将现有对象转为一个 Promise 对象,也可用于迅速的创建对象
方法参数(此处参考 阮一峰的 ECMAScript 6 入门 : Promise对象)
- 参数是一个Promise实例,将直接返回实例
- 参数是一个 thenable 对象,Promise.resolve方法会将这个对象转为新的 Promise 然后就立即执行 thenable 对象的then方法
- 如果参数是一个原始值,或者是一个不具有then方法的对象,再或不带参数,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved,传递给下一步 then 的值为参数
Promise.reject()
返回一个状态为 rejected 的新 Promise 实例,参数用法与Promise.resolve方法一致(注意如果参数是一个 Promise, reject 依旧会返回一个新 Promise)。
Promise.all()
将多个 Promise 实例,包装成一个新的Promise实例: var p = Promise.all([p1, p2, p3]);
- 数组参数如果不是 Promise,调用 Promise.resolve() 方法转化
- 参数可以不是数组,但必须具有Iterator接口
- 并发执行
返回
- 只有全部状态均变为 fulfilled, p 才会变为 fulfilled
- 只要有一个 rejected, p 就以 第一个 reject 的值 进入 rejected
- fulfill 得到的结果是一个与输入 Promise 顺序相同的数组
Promise.race()
将多个Promise实例,包装成一个新的Promise实例 : var p = Promise.race([p1,p2,p3]);
- 只要有一个先改变状态,p 即 随之变化
- 其他同 Promise.all()
还记得吗? Promise 无法取消已执行的任务
所以即使有一个 promise 率先返回, 其他 promise 依旧会执行
这一点对于上述的 Promise.all 如果也成立
不属于 ES6 规范但有用的两个方法
done()
- 保证可以抛出之前任何可能的未处理错误
- 实现其实非常简单,如下
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
setTimeout(() => { throw reason }, 0);
});
};
finally()
- 不管 promise 最后状态如何,一定会执行的操作
- 实现也很简单
Promise.prototype.finally = function (callback) {
let p = this.constructor;
return this.then(
value => p.resolve(callback()).then(() => value),
reason => p.resolve(callback()).then(() => { throw reason })
);
};
实现与测试
Promise 简单的实现并不难,建议自己独立实现一次
- 如果你完整的实现了 Promise/A+ 标准,可以尝试使用
promises-aplus-tests MyPromise.js
去测试是否成功(需要安装),测试具体可见 promises-aplus-tests
通过测试不是很难,代码可以参考我的 github - 另一个测试为 promises-es6-tests,实现此测试需要阅读 ECMA-262 6th Edition Draft specification
一些有关实现 Promise 的参考文章
- 剖析 Promise 之基础篇 和 JavaScript Promises … In Wicked Detail
简单的实现,没有实现 Promise/A+ - Promise implements : 相对而言较复杂的实现,实现了 Promises/A+
- hax(贺师俊)的 hax/my-promise : 可以通过 promises-es6-tests 和 promises-aplus-tests
一个小测试
如下四种写法有什么区别?
doSomething().then(function () {
return doSomethingElse();
});
doSomething().then(function () {
doSomethingElse();
});
doSomething().then(doSomethingElse());
doSomething().then(doSomethingElse);
答案:
- 要注意的是 then 第一个函数中没有参数代表 doSomething 函数的返回值
- 没有返回 doSomethingElse 函数的返回值
- then 里放的是 doSomethingElse 的返回值,还记得如果不是函数就会忽略这个参数吧?所以此 promise 下一个 then 中接到的值依旧是 doSomething 的返回值
- 最后一个,相当于 doSomethingElse 就是 onFulfilled 函数, doSomethingElse 函数的参数是 doSomething 的返回值,doSomethingElse 的返回值将传递下去
此问题来自We have a problem with promises,此篇文章写的很好,讲了常见的一些 Promise 的使用误区
你也可以看其翻译版: 谈谈使用 promise 时候的一些反模式
其他
- Promise 并不总是处理异步编程的最佳形式,比如有时需要用到 stream 模式
- 本文没有提到 Deferred,不建议使用它,是 Promise anti patterns(参考:Promise anti patterns)
除了上述提到的文章外,本篇还参考了:
- Promise : MDN 文档
JavaScript Promise迷你书(中文版)。 此书的 PDF 版有 110 页,比较基础(如果是 javascript 初学者可以过一遍)