原文: https://blog.coding.net/blog/how-do-promises-work
Javascript 采用回调函数(callback)来处理异步编程。从同步编程到异步回调编程有一个适应的过程,但是如果出现多层回调嵌套,也就是我们常说的厄运的回调金字塔(Pyramid of Doom),绝对是一种糟糕的编程体验。于是便有了 CommonJS 的 Promises/A 规范,用于解决回调金字塔问题。本文先介绍 Promises 相关规范,然后再通过解读一个迷你的 Promises 以加深理解。
什么是 Promise
一个 Promise 对象代表一个目前还不可用,但是在未来的某个时间点可以被解析的值。它允许你以一种同步的方式编写异步代码。例如,如果你想要使用 Promise API 异步调用一个远程的服务器,你需要创建一个代表数据将会在未来由 Web 服务返回的 Promise 对象。唯一的问题是目前数据还不可用。当请求完成并从服务器返回时数据将变为可用数据。在此期间, Promise 对象将扮演一个真实数据的代理角色。接下来,你可以在 Promise 对象上绑定一个回调函数,一旦真实数据变得可用这个回调函数将会被调用。
Promise 对象曾经以多种形式存在于许多语言中。
去除厄运的回调金字塔(Pyramid of Doom)
Javascript 中最常见的反模式做法是回调内部再嵌套回调。
// 回调金字塔
asyncOperation(function(data){
// 处理 `data`
anotherAsync(function(data2){
// 处理 `data2`
yetAnotherAsync(function(){
// 完成
});
});
});
引入 Promises 之后的代码
promiseSomething()
.then(function(data){
// 处理 `data`
return anotherAsync();
})
.then(function(data2){
// 处理 `data2`
return yetAnotherAsync();
})
.then(function(){
// 完成
});
Promises 将嵌套的 callback ,改造成一系列的.then
的连缀调用,去除了层层缩进的糟糕代码风格。 Promises 不是一种解决具体问题的算法,而已一种更好的代码组织模式。接受新的组织模式同时,也逐渐以全新的视角来理解异步调用。
各个语言平台都有相应的 Promise 实现
- Java’s java.util.concurrent.Future
- Python’s Twisted deferreds and PEP-3148 futures
- F#'s Async<T>
- .Net’s Task<T>
- C++ 11’s std::future
- Dart’s Future<T>
- Javascript’s Promises/A/B/D/A+
下面我来相信了解一下 javascript 语言环境下各个规范的一些细节。
Promises/A 规范
promise 表示一个最终值,该值由一个操作完成时返回。
- promise 有三种状态:未完成 (unfulfilled),完成 (fulfilled) 和失败 (failed)。
- promise 的状态只能由未完成转换成
完成
,或者未完成转换成失败 。 - promise 的状态转换只发生一次。
promise 有一个 then 方法, then 方法可以接受 3 个函数作为参数。前两个函数对应 promise 的两种状态 fulfilled 和 rejected 的回调函数。第三个函数用于处理进度信息(对进度回调的支持是可选的)。
promiseSomething().then(function(fulfilled){
//当 promise 状态变成 fulfilled 时,调用此函数
},function(rejected){
//当 promise 状态变成 rejected 时,调用此函数
},function(progress){
//当返回进度信息时,调用此函数
});
如果 promise 支持如下连个附加方法,称之为可交互的 promise
- get(propertyName) 获得当前 promise 最终值上的一个属性,返回值是一个新的 promise 。
- call(functionName, arg1, arg2, …) 调用当然 promise 最终值上的一个方法,返回值也是一个新的 promise 。
Promises/B 规范
在 Promises/A 的基础上, Promises/B 定义了一组 promise 模块需要实现的 API
when(value, callback, errback_opt)
如果 value 不是一个 promise ,那么下一事件循环 callback 会被调用, value 作为 callback 的传入值。如果 value 是一个 promise , promise 的状态已经完成或者变成完成时,那么下一事件循环 callback 会被调用, resolve 的值会被传入 callback ; promise 的状态已经失败或者变成失败时,那么下一事件循环 errback 会被调用, reason 会作为失败的理由传入 errback 。
asap(value, callback, errback_opt)
与 when 最大的区别,如果 value 不是一个 promise ,会被立即执行,不会等到下一事件循环。
enqueue(task Function)
尽可能快地在接下来的事件循环调用 task 方法。
get(object, name)
返回一个获得对象属性的 promise 。
post(object, name, args)
返回一个调用对象方法的 promise 。
put(object, name, value)
返回一个修改对象属性的 promise 。
del(object, name)
返回一个删除对象属性的 promise 。
makePromise(descriptor Object, fallback Function)
返回一个 promise 对象,该对象必须是一个可调用的函数,也可能是可被实例化的构造函数。
- 第一个参数接受一个描述对象,该对象结构如下,
{
"when": function(errback){...},
"get": function(name){...},
"put": function(name, value){...},
"post": function(name, args){...},
"del": function(name){...},
}
上面每一个注册的 handle 都返回一个 resolved value 或者 promise 。
- 第二个参数接受一个 fallback(message,…args) 函数,当没有 promise 对象没有找到对应的 handle 时该函数会被触发,返回一个 resolved value 或者 promise 。
defer()
返回一个对象,该对象包含一个 resolve(value) 方法和一个 promise 属性。
当 resolve(value) 方法被第一次调用时, promise 属性的状态变成 完成,所有之前或之后观察该 promise 的 promise 的状态都被转变成 完成。 value 参数如果不是一个 promise ,会被包装成一个 promise 的 ref 。 resolve 方法会忽略之后的所有调用。
reject(reason String)
返回一个被标记为 失败 的 promise 。
一个失败的 promise 上被调用 when(message) 方法时,会采用如下两种方法之一
- 如果存在 errback , errback 会以 reason 作为参数被调用。 when 方法会将 errback 的返回值返回。
- 如果不存在 errback , when 方法返回一个新的 reject 状态的 promise 对象,以同一 reason 作为参数。
ref(value)
如果 value 是 promise 对象,返回 value 本身。否则,返回一个 resolved 的 promise ,携带如下 handle 。
- when(errback),忽略 errback ,返回 resolved 值
- get(name),返回 resolved 值的对应属性。
- put(name, value) ,设置 resolved 值的对应属性。
- del(name),删除 resolved 值的对应属性。
- post(name, args), 调用 resolved 值的对应方法。
- 其他所有的调用都返回一个 reject ,并携带 “Promise does not handle NAME” 的理由。
isPromise(value) Boolean
判断一个对象是否是 promise
method(name String)
获得一个返回 name 对应方法的 promise 。返回值是 “get”, “put”, “del” 和 “post” 对应的方法,但是会在下一事件循环返回。
Promises/D 规范
为了增加不同 promise 实现之间的可互操作性, Promises/D 规范对 promise 对象和 Promises/B 规范做了进一步的约定。以达到鸭子类型的效果( Duck-type Promise )。
简单来说 Promises/D 规范,做了两件事情,
- 如何判断一个对象是 Promise 类型。
- 对 Promises/B 规范进行细节补充。
甄别一个 Promise 对象
Promise 对象必须是实现 promiseSend
方法。
- 在 promise 库上下文中,如果对象包含
promiseSend
方法就可以甄别为 promise 对象 promiseSend
方法必须接受一个操作名称,作为第一个参数- 操作名称是一个可扩展的集合,下面是一些保留名称
when
,此时第三个参数必须是 rejection 回调。- rejection 回调必须接受一个 rejection 原因(可以是任何值)作为第一个参数
get
,此时第三个参数为属性名(字符串类型)put
,此时第三个参数为属性名(字符串类型),第四个参数为新属性值。del
,此时第三个参数为属性名post
,此时第三个参数为方法的属性名,接下来的变参为方法的调用参数isDef
promiseSend
方法的第二个参数为 resolver 方法promiseSend
方法可能接受变参promiseSend
方法必须返回undefined
对 Promises/B 规范的补充
Promises/D 规范中对 Promises/B 规范中定义的 ref 、 reject 、 def 、 defer 方法做了进一步细致的约束,此处略去这些细节。
Promises/A+ 规范
前面提到的 Promises/A/B/D 规范都是有 CommonJS 组织提出的, Promises/A+是有一个自称为Promises/A+ 组织发布的,该规范是以 Promises/A 作为基础进行补充和修订,旨在提高 promise 实现之间的可互操作性。
Promises/A+ 对.then
方法进行细致的补充,定义了细致的Promise Resolution Procedure流程,并且将.then
方法作为 promise 的对象甄别方法。
此外, Promises/A+ 还提供了兼容性测试工具,以确定各个实现的兼容性。
实现一个迷你版本的 Promise
上面扯了这么多规范,现在我们看看如何实现一个简单而短小的 Promise 。
状态机
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
function Promise() {
// store state which can be PENDING, FULFILLED or REJECTED
var state = PENDING;
// store value or error once FULFILLED or REJECTED
var value = null;
// store sucess & failure handlers attached by calling .then or .done
var handlers = [];
}
状态变迁
仅支持两种状态变迁, fulfill 和 reject
// ...
function Promise() {
// ...
function fulfill(result) {
state = FULFILLED;
value = result;
}
function reject(error) {
state = REJECTED;
value = error;
}
}
fulfill 和 reject 方法较为底层,通常更高级的 resolve 方法开放给外部。
// ...
function Promise() {
// ...
function resolve(result) {
try {
var then = getThen(result);
if (then) {
doResolve(then.bind(result), resolve, reject)
return
}
fulfill(result);
} catch (e) {
reject(e);
}
}
}
resolve 方法可以接受一个普通值或者另一个 promise 作为参数,如果接受一个 promise 作为参数,等待其完成。 promise 不允许被另一个 promise fulfill ,所以需要开放 resolve 方法。 resolve 方法依赖一些帮助方法定义如下:
/**
* Check if a value is a Promise and, if it is,
* return the `then` method of that promise.
*
* @param {Promise|Any} value
* @return {Function|Null}
*/
function getThen(value) {
var t = typeof value;
if (value && (t === 'object' || t === 'function')) {
var then = value.then;
if (typeof then === 'function') {
return then;
}
}
return null;
}
/**
* Take a potentially misbehaving resolver function and make sure
* onFulfilled and onRejected are only called once.
*
* Makes no guarantees about asynchrony.
*
* @param {Function} fn A resolver function that may not be trusted
* @param {Function} onFulfilled
* @param {Function} onRejected
*/
function doResolve(fn, onFulfilled, onRejected) {
var done = false;
try {
fn(function (value) {
if (done) return
done = true
onFulfilled(value)
}, function (reason) {
if (done) return
done = true
onRejected(reason)
})
} catch (ex) {
if (done) return
done = true
onRejected(ex)
}
}
这里 resolve 和 doResolve 之间的递归很巧妙,用来处理 promise 的层层嵌套( promise 的 value 是一个 promise )。
构造器
// ...
function Promise(fn) {
// ...
doResolve(fn, resolve, reject);
}
.done 方法
// ...
function Promise(fn) {
// ...
function handle(handler) {
if (state === PENDING) {
handlers.push(handler);
} else {
if (state === FULFILLED &&
typeof handler.onFulfilled === 'function') {
handler.onFulfilled(value);
}
if (state === REJECTED &&
typeof handler.onRejected === 'function') {
handler.onRejected(value);
}
}
}
this.done = function (onFulfilled, onRejected) {
// ensure we are always asynchronous
setTimeout(function () {
handle({
onFulfilled: onFulfilled,
onRejected: onRejected
});
}, 0);
}
// ...
}
.then 方法
// ...
function Promise(fn) {
// ...
this.then = function (onFulfilled, onRejected) {
var self = this;
return new Promise(function (resolve, reject) {
return self.done(function (result) {
if (typeof onFulfilled === 'function') {
try {
return resolve(onFulfilled(result));
} catch (ex) {
return reject(ex);
}
} else {
return resolve(result);
}
}, function (error) {
if (typeof onRejected === 'function') {
try {
return resolve(onRejected(error));
} catch (ex) {
return reject(ex);
}
} else {
return reject(error);
}
});
});
}
// ...
}
$.promise
jQuery 1.8 之前的版本, jQuery 的 then 方法只是一种可以同时调用 done 、 fail 和 progress 这三种回调的速写方法,而 Promises/A 规范的 then 在行为上更像是 jQuery 的 pipe 。 jQuery 1.8 修正了这个问题,使 then 成为 pipe 的同义词。不过,由于向后兼容的问题, jQuery 的 Promise 再如何对 Promises/A 示好也不太会招人待见。
此外,在 Promises/A 规范中,由 then 方法生成的 Promise 对象是已执行还是已拒绝,取决于由 then 方法调用的那个回调是返回值还是抛出错误。在 JQuery 的 Promise 对象的回调中抛出错误是个糟糕的主意,因为错误不会被捕获。
小结
最后一个例子揭示了,实现 Promise 的关键是实现好 doResolve 方法,在完事以后触发回调。而为了保证异步 setTimeout(fun, 0);
是关键一步。
Promise 一直用得蛮顺手的,其很好的优化了 NodeJS 异步处理时的代码结构。但是对于其工作原理却有些懵懂和好奇。于是花了些经理查阅并翻译了 Promise 的规范,以充分的理解 Promise 的细节。
参考阅读
- Promises/A
- Promises/B
- Promises/D
- Promisejs
- Promises/A+
- As soon as possible
- A minimalist implementation of a javascript promise
- Lightweight implementation of promises
- How is a promise/defer library implemented?
- Basic Javascript promise implementation attempt
- You’re Missing the Point of Promises
- Boom! Promises/A+ Was Born
- Futures and promises
- JavaScript Promises - There and back again
Vangie Du
将来的你,一定会感谢现在拼命努力的自己!