使用Promise是极好的,它是如此有用以至于我觉得应该好好研究一下Promise,甚至是实现一个简易的版本。实现之前,我们先来看看Promise的用途:
使用Promise
callback hell
Promise的第一个用途是能够很好地解决的问题,假设要实现一个用户展示的任务,这个任务分为三步:
获取用户信息
获取用户图像
弹窗提示
不使用Promise,我们的实现可能是这样子:
getUserInfo(id, function (info) { getUserImage(info.img, function () { showTip(); })})
这里只是三步,如果有更长串的任务时,我们就会陷入到回调黑洞之中,为了解决这个问题,我们就可以使用Promise来处理这一长串任务,使用Promise的版本是这样子的:
// getUserInfo返回promisegetUserInfo(id) .then(getUserImage) .then(showTip) .catch(function (e) { console.log(e); });
原来向右发展的代码,开始向下发展,这样也更适合编程习惯,如果要让我们的代码更加健壮,我们就需要在每一步来处理错误信息,使用promise这后,我们只需要在最后的catch中做善后处理。
并发
假如我们要显示某一个页的10条记录,但是我们只有一个通过id获取记录的接口,这样我们就需要发送10个请求,并且所有请求都完成之后再将记录全部添加到页面之中,Promise在这个场景下使用是特别合适的。
代码可能是这样子:
// ids要获取信息的所有记录id// getRecordById获取记录的接口,返回promisePromise.all(ids.map(getRecordById)) .then(showRecords) .catch(function (e) { console.log(e); });
这就是Promise的一些简单的用途,当然令人兴奋的是Promise已经是ES6的标准,而且目前很多浏览器已经原生支持Promise了。对于那些无法使用Promise的浏览器,我们就只能自己去实现了,下面就来看看Promise的简单实现吧。
实现
warm up
先来盗用一张MDN的图,先来热热身,看看Promise的状态迁移:
Promise有三种状态:
pending:初始状态, 非 fulfilled 或 rejected
fulfilled: 成功的操作
rejected: 失败的操作
我们可以看出新建的Promise是pending状态,fulfill之后就会执行调用then的回调函数了,倘若reject了就会调用catch来进行异常处理了,并且无论是调用then还是catch都会返回新的promise,这就是为什么promise可以链式调用了。
接着,我们来研究一下规范是怎么描述
的。这里只抽取核心部分,边界问题不考虑。构造函数:Promise ( executor )
检查参数:例如executor是不是函数啊
初始化:
[[State]]=pending
,[[FulfillReactions]]=[]
,[[RejectReactions]]=[]
创建resolve对象:
{[[Resolve]]: resolve, [[Reject]]: reject}
执行executor:
executor(resolve, reject)
因此构造函数里面传入的excuter是立即被执行的。FulfillReactions存储着promise执行成功时要做的操作,RejectReactions存储着promise是要执行的操作。
function Promise(resolver) { this._id = counter++; this._state = PENDING; this._result = undefined; this._subscribers = []; var promise = this; if (noop !== resolver) { try { resolver(function (value) { resolve(promise, value); }, function (reason) { reject(promise, reason); }); } catch (e) { reject(promise, e); } }}
FulfillPromise(promise, value)
检查[[state]],必须为pending(不是pending的表示已经解析,不能重复解析)
赋值:
[[Result]]=value
,[[state]]=fulfilled
触发[[FulfillReactions]]的操作
和FulfillPromise联系最紧密的就是ResolvePromise了,这里我们给出的是ResolvePromise的实现,区别只是多了直接解析Promise。
function resolve(promise, value) { // 要resolve的为promise(then的callback返回的是promise) if (typeof value === 'object' && promise.constructor === value.constructor) { handleOwnThenable(promise, value); } // 要resolve的是值 else { if (promise._state !== PENDING) { return; } promise._result = value; promise._state = FULFILLED; asap(publish, promise); }}function handleOwnThenable(promise, thenable) { // 如果返回的promise已经完成 // 直接用该promise的值resolve父promise if (thenable._state === FULFILLED) { resolve(promise, thenable._result); } else if (thenable._state === REJECTED) { reject(promise, thenable._result); } // 如果返回的promise未完成 // 要等该promise完成再resolve父promise else { subscribe(thenable, undefined, function(value) { resolve(promise, value); }, function(reason) { reject(promise, reason); }); }}
RejectPromise(promise, reason)
检查[[state]],必须为pending(不是pending的表示已经解析,不能重复解析)
赋值:
[[Result]]=reason
,[[state]]=rejected
触发[[RejectReactions]]的操作
触发[[FulfillReactions]]和触发[[RejectReactions]]实际就是遍历数组,执行所有的回调函数。
function reject(promise, reason) { if (promise._state !== PENDING) { return; } promise._state = REJECTED; promise._result = reason; asap(publish, promise);}
Promise.prototype.then(onFullfilled, onRejected)
promise=this
新建resultCapability三元组,
{[[Promise]], [[Resolve]], [[Reject]]}
([[Promise]]新建的)fulfillReaction={[[Capabilities]]: resultCapability, [[Handler]]: onFulfilled}
rejectReaction={[[Capabilities]]: resultCapability, [[Handler]]: onRejected}
如果[[state]]是pending:fulfillReaction加入[[FulfillReactions]],rejectReaction加入[[RejectReactions]]
如果[[state]]是fulfilled:fulfillReaction加入执行队列
如果[[state]]是rejected:rejectReaction加入执行队列
返回resultCapability.[[Promise]]
这里可以看出构造函数和then的关系是很紧密的,新建的promise如果是异步操作,那么状态就是pending,调用then时会新建子promise,并且将回调操作加入父promise的[[FulfillReactions]]或[[RejectReactions]]的数组里,这实际就是发布订阅模式。
他们是这样的关系:
无论是new promise还是调用then或catch,都会得到一个新的promise,这些promise都会订阅父级promise的完成事件,父级promise完成之后就会执行一系列的回调操作,也就是发布。
Promise.prototype.catch(onRejected)
then的语法糖:
then(null, onRejected)
下面就是Promise原型:
Promise.prototype = { constructor: Promise, then: function (onFulfillment, onRejection) { var parent = this; var state = parent._state; if (state === FULFILLED && !onFulfillment || state === REJECTED && !onRejection) { return this; } var child = new Promise(noop); var result = parent._result; if (state) { var callback = arguments[state - 1]; asap(function () { invokeCallback(state, child, callback, result); }); } else { subscribe(parent, child, onFulfillment, onRejection); } return child; }, 'catch': function (onRejection) { return this.then(null, onRejection); }};
Promise.resolve(value)
新建promise
调用
ResolvePromise(promise, value)
(未列出,会判断一些情况然后调用FulfillPromise)返回promise
Promise.resolve = function (arg) { var child = new Promise(noop); resolve(child, arg); return child;};
Promise.reject(value)
新建promise
调用
RejectPromise(promise, value)
返回promise
Promise.reject = function (reason) { var child = new Promise(noop); reject(child, reason); return child;};
Promise.all(iterator)
到这里我们已经能够实现基本的promise了,Promise.all
和Promise.race
就不继续描述了,有兴趣的可以继续去读规范,这里上图来说明我对这两个函数的理解:
调用promise.all会新建一个对象来存储所有promise的处理状态,保存执行的结果,当remain为0时,就可以resolve 新建的promise,这样就可以继续往后执行了。
Promise.all = function (promises) { var child = new Promise(noop); var record = { remain: promises.length, values: [] }; promises.forEach(function (promise, i) { if (promise._state === PENDING) { subscribe(promise, undefined, onFulfilled(i), onRejected); } else if (promise._state === REJECTED) { reject(child, promise._result); return false; } else { --record.remain; record.values[i] = promise._result; if (record.remain == 0) { resolve(child, values); } } }); return child; function onFulfilled(i) { return function (val) { --record.remain; record.values[i] = val; if (record.remian === 0) { resolve(child, record.values); } } } function onRejected(reason) { reject(child, reason); }};
Promise.race(iterator)
promise.race与promise.all类似,不过只要有一个promise完成了,我们就可以resolve新建的promise了。
Promise.race = function (promises) { var child = new Promise(noop); promises.forEach(function (promise, i) { if (promise._state === PENDING) { subscribe(promise, undefined, onFulfilled, onRejected); } else if (promise._state === REJECTED) { reject(child, promise._result); return false; } else { resolve(child, promise._result); return false; } }); return child; function onFulfilled(val) { resolve(child, val); } function onRejected(reason) { reject(child, reason); }};
这就是promise的基本内容了,完整代码请戳。
其他问题
promises 穿透
如果传入then里面的参数不是函数,就会被忽略,这就是promise穿透的原因,所以永远往then里面传递函数。答案可以从then方法里面调用的一个关键函数invokeCallback中找到答案:
function invokeCallback(settled, promise, callback, detail) { var hasCallback = (typeof callback === 'function'), value, error, succeeded, failed; if (hasCallback) { try { value = callback(detail); } catch (e) { value = { error: e }; } if (value && !!value.error) { failed = true; error = value.error; value = null; } else { succeeded = true; } } // then的参数不是函数 // 会被忽略,也就是promise穿透 else { value = detail; succeeded = true; } if (promise._state === PENDING) { if (hasCallback && succeeded || settled === FULFILLED) { resolve(promise, value); } else if (failed || settled === REJECTED) { reject(promise, error); } } }
例如如下例子,结果都是输出foo:
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) { console.log(result);});Promise.resolve('foo').then(null).then(function (result) { console.log(result);});
拥抱金字塔
promise能够很好的解决金字塔问题,但是有时候我们也是需要适当使用金字塔的,例如我们要同时获取两个promise的结果,但是这两个promise是有关联的,也就是有顺序的,该怎么办?
也许解决方案会是这样,定义一个全局变量,这样在第二个then里面就可以使用两个promise的结果了。
var user;getUserByName('nolan').then(function (result) { user = result; return getUserAccountById(user.id);}).then(function (userAccount) { // 好了, "user" 和 "userAccount" 都有了});
但是这不是最好的方案,此时何不抛弃成见,拥抱金字塔:
getUserByName('nolan').then(function (user) { return getUserAccountById(user.id).then(function (userAccount) { // 好了, "user" 和 "userAccount" 都有了 });});
promise是如此强大而且难以理解,但是抓住实质之后其实并没有想象的那么复杂,这也是为什么我要写下这篇文章。更过关于如何正确使用promise,请看第三篇,强力推荐。