在前文中,我们已经搭建好了 Promise 的一个基本“外形”。
var Promise = function (executor) {
this._state = null;
// ToDo: Implement it.
};
Promise.prototype.then = function (onfulfilled, onrejected) {
return Promise(function (resolve, reject) {
// ToDo: Implement it.
});
};
Promise.prototype.catch(onrejected) {
return this.then(null, onrejected);
}
现在,我们要开始具体实现这些待实现的代码了。
由于在构造函数中,传入的主要业务逻辑函数中,需要传入两个参数,即成功和失败的触发器,因此,我们先实现这两个触发器。那么,成功触发器里需要做一些什么呢?
- 检查状态。如果当前不处于等待状态,则不做任何事情。
- 检查传入的参数是不是该 Promise 自己,是的话是不支持的。
- 将状态设置为执行成功,同时保存运算结果。
- 唤起所有注册的成功事件。
- 我们需要通过 try-catch 方式,将此时出错的情形,引导至失败的处理方式中去。
Promise.prototype._resolve = function (value) {
if (this._state !== null) return;
if (value === this) throw new TypeError('A promise cannot be resolved with itself.');
this._state = true;
this._value = value as T;
// ToDo: 唤起所有回调。
}
当然,刚才说过,需要一个 try-catch 将里面的代码包起来。
Promise.prototype._resolve(value) {
try {
// 刚才里面的代码。
} catch (e) {
// ToDo: 失败响应。
}
}
可是,你可能会意识到,如果传入的参数,其实是一个其它的 Promise 对象怎么办?显然我们也计划支持这种场景,即此时应当自动将下一个返回的 Promise 对象用传入的代替,因此我们先需要有一个参数将这种情形记录下来,以便之后检查。我们需要在构造函数里再加入一个属性。
this._delegating = false;
然后,在刚才的 _resolve 方法里,在设置状态之前,以及校验传入参数是否自己之后,加上以下代码。
if (value && (typeof value === 'object' || typeof value === 'function') && typeof (value.then === "function") {
this._delegating = true;
value.then(this._resolve, this._reject);
return;
}
由此,成功状态的判断和前置处理就完成了。那么,我们继续实现失败响应。其实和成功响应差不多,无非就是先判断当前状态是否为等待中,如果是的话,则将状态更新为失败,并记录出错信息,然后唤起所有回调。
Promise.prototype._reject = function (reason) {
if (this._state !== null) return
this._state = false;
this._value = reason;
// ToDo: 唤起所有回调。
}
在这里,我们都预留了唤起所有回调没有实现。要实现这个,我们需要先找个地方记录这些回调及相关信息。由于每组回调都可以包含成功和失败之后的处理,并且都可以有返回值做为下一个 Promise 的结果,因此,针对每组回调,我们都需要保存以下信息。
- 成功时的回调。
- 失败时的回调。
- 成功时,下一个 Promise 对象的处理过程。
- 失败时,下一个 Promise 对象的处理过程。
我们需要在构造函数里放一个数组来存储所有的这些信息。
this._deferreds = [];
然后,针对每一组这个信息,我们需要一个通用处理方法,能够在当下不同状态进行不同的处理:主要业务逻辑结束前进行记录;以及,在结束后进行执行,但考虑到这时延迟执行的,因此结束后需要等待当前线程正在运作的事务执行完后,才开始执行,故需要使用一个回调。
Promise.prototype._handle = function (deferred) {
if (this._state === null) {
this._deferreds.push(deferred);
return;
}
setTimeout(() => {
// ToDo: 结束后执行的回调。
}, 0);
}
对于此处的结束后执行的回调,我们需要做这些事情。
- 检查状态,以确定到底是执行成功的回调还是失败的回调。
- 如果没有对应的回调,则直接执行当前的成功或失败的路由。
- 在 try-catch 包内,触发对应的回调,若失败,则转入失败处理。
代码如下。
// 根据状态获取对应的回调。
var cb = this._state ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
// 如果并无对应的回调,则使用当前默认的回调。
(this._state ? deferred.resolve : deferred.reject)(this._value as any);
return;
}
// 唤起注册的回调。
var ret: TResult | PromiseLike<TResult>;
try {
ret = cb(this._value) as any;
}
catch (e) {
deferred.reject(e);
return
}
// 成功执行。
deferred.resolve(ret);
现在,我们已经能够处理单组注册事件了。在前面声明的 then 方法中,我们也需要调用这个方法。如下代码。
Promise.prototype.then = function (onfulfilled, onrejected) {
return Promise(function (resolve, reject) {
this._handle<TResult>({
onFulfilled: onfulfilled,
onRejected: onrejected,
resolve: resolve,
reject: reject
});
});
};
好。现在回到刚才我们还没实现的唤起所有回调。我们只需写一个函数,里面放一个循环,就能轻松搞定。
Promise.prototype._finale = function () {
for (var i = 0, len = this._deferreds.length; i < len; i++)
this._handle(this._deferreds[i]);
this._deferreds = null;
}
于是,刚才那些唤起所有回调的部分,只需调用上面这个函数即可。剩下的事情就是把构造函数完成了。
try {
executor((value) => {
if (!this._delegating) this._resolve(value)
}, (reason) => {
if (!this._delegating) this._reject(reason)
});
} catch (e) {
if (!this._delegating) this._reject(e)
}
现在,Promise 初步完成了。当然,由于这里是为了说清楚原理,大量使用 Prototype 来定义一些方法;但,或许你想隐藏一些私有的成员属性和方法,那么你可以依此进行一些改造,例如把许多东西都放入构造函数内实现,这里就不再继续介绍了。那么,接下来还有什么呢?其实,Promise 还有一些辅助方法可以帮助我们处理一些多任务的事情,后面的文章会以其中一个例子,来解释这是如何实现的。
【未完待续】
文章类型及复杂度:Web 前端开发进阶。
节选翻译自 MSDN 博文 JavaScript Promise,内容有所调整。
http://blogs.msdn.com/b/kingcean/archive/2016/03/25/promise-in-web.aspx