实现 Promise
使用 Promise 是极好的,它是如此有用以至于我觉得应该好好研究一下 Promise,甚至是实现一个简易的版本。实现之前,我们先来看看 Promise 的用途:
使用 Promise
callback hell
Promise 的第一个用途是能够很好地解决 回调黑洞 的问题,假设要实现一个用户展示的任务,这个任务分为三步:
- 获取用户信息
- 获取用户图像
- 弹窗提示
不使用 Promise,我们的实现可能是这样子:
getUserInfo(id, function (info) {
getUserImage(info.img, function () {
showTip();
})
})
这里只是三步,如果有更长串的任务时,我们就会陷入到回调黑洞之中,为了解决这个问题,我们就可以使用 Promise 来处理这一长串任务,使用 Promise 的版本是这样子的:
// getUserInfo 返回 promise
getUserInfo(id)
.then(getUserImage)
.then(showTip)
.catch(function (e) {
console.log(e);
});
原来向右发展的代码,开始向下发展,这样也更适合编程习惯,如果要让我们的代码更加健壮,我们就需要在每一步来处理错误信息,使用 promise 这后,我们只需要在最后的 catch 中做善后处理。
并发
假如我们要显示某一个页的 10 条记录,但是我们只有一个通过 id 获取记录的接口,这样我们就需要发送 10 个请求,并且所有请求都完成之后再将记录全部添加到页面之中,Promise 在这个场景下使用是特别合适的。
代码可能是这样子:
// ids 要获取信息的所有记录 id
// getRecordById 获取记录的接口,返回 promise
Promise.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 的。这里只抽取核心部分,边界问题不考虑。
构造函数: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,请看第三篇 参考文章 ,强力推荐。
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论