前言
最近整理了两篇关于js异步的笔记,谈到异步就不得不说说 Promise。Promise 取代传统回调方式实现异步,也是理解 generator、async/await 的前提。
基本用法不再阐述,这里只谈谈如何从无到有的实现 Promise。
一、从构造函数开始
我们在使用 Promise 时都要使用 new 关键字,由此可知 Promise 其实就是一个构造函数,我们使用这个构造函数创建一个 Promise 实例。
该构造函数很简单,它只有一个参数,按照 Promise/A+ 规范的命名,把 Promise 构造函数的参数叫做 executor,executor 类型为函数。这个函数又天生的具有 resolve、reject 两个方法作为参数。
构造函数的定义如下:
function Promise(executor) {
}
二、初见本质
通过一个简单的例子复习下基本的用法
var getPic = url =>
new Promise((resolve, reject) => {
// ... some code
if (/* 异步操作成功 */) {
resolve("success value");
} else {
/* 异步操作失败 */
reject("sth wrong");
}
});
getPic("1.png").then(
data => {
console.log(`加载完成${data}`);
},
error => {
console.log(error);
}
);
观察例子,剖析本质
- Promise 构造函数返回一个 promise 对象实例,这个返回的 promise 对象具有一个 then 方法。
- 由 Promise/A+ 规范可知,then 方法中有两个参数,都是函数类型,分别是 onfulfilled 和 onrejected。**
- 函数 onfulfilled 可以通过参数获取到 promise 对象 resolved 的值,onrejected 函数可以通过参数获取到 promise 对象的 rejected 的值。**
在此基础上继续完善我们的 Promise,添加原型方法 then
function Promise(executor) {
}
Promise.prototype.then = function(onfulfilled, onrejected) {
};
这里可能有人会有疑问,then 方法为何要添加在 Promise 构造函数的原型上,而不是作为实例方法?
简单来说就是每个 promise 实例的 then 方法逻辑是完全一致的,在实例调用该方法时,可以通过原型(Promise.prototype)找到,如果作为实例方法,那么每次实例化都会为每个实例新创建一个 then 方法,而这些 then 方法又是一样的,这样显示不合理,并且会浪费内存。
总结:
通过 new 关键字调用 Promise 构造函数时,在执行完逻辑后,调用上文所说的构造函数参数 executor 中的 resolve 方法,并将需要返回的值作为 resolve 函数参数执行。并且这个返回的值,可以在 then 方法的第一个参数(onfulfilled函数)中拿到。
同理当出现错误时调用 executor 中的 reject 方法,也可将需要返回的错误信息作为 reject 函数参数执行。这个错误的信息就可以在 then 方法的第二个参数(onrejected函数)中拿到。
那么我们就需要俩个变量,分别存储 resolve 的值和 reject 的值。与此同时,还需要一个变量来存储 promise 的状态(pending、fulfilled、rejected)。最后构造函数中还要有 resolve 以及 reject 方法,并且要作为构造函数参数(executor)的参数提供给调用者使用。
根据我们总结的点继续完善 Promise :
function Promise(excutor) {
this.status = "pending";
this.resolveVal = null;
this.rejectVal = null;
const resolve = value => {
this.resolveVal = value;
};
const reject = error => {
this.rejectVal = error;
};
excutor(resolve, reject);
}
Promise.prototype.then = function(
onfulfilled = Function.prototype,
onrejected = Function.prototype
) {
onfulfilled(this.resolveVal);
onrejected(this.rejectVal);
};
三、状态管理不可少
promise 实例的状态具有单一,不可逆的特征,就是说 promise 实例的状态只能从 pending 改变为 fulfilled,或者从 pending 改变为 rejected,并且一旦改变,不能再次更改。不过我们目前的实现无法满足这一需求,如果我们先后调用 resolve('value'); reject('error');
,status 的状态会先更改为 fulfilled 之后再变更为 rejected。then 方法中的两个函数参数都会执行。
要在代码中加入状态的变更以及判断:
function Promise(excutor) {
this.status = "pending";
this.resolveVal = null;
this.rejectVal = null;
const resolve = value => {
if (this.status === "pending") {
this.resolveVal = value;
this.status = "fulfilled";
}
};
const reject = error => {
if (this.status === "pending") {
this.rejectVal = error;
this.status = "rejected";
}
};
excutor(resolve, reject);
}
Promise.prototype.then = function(
onfulfilled = Function.prototype,
onrejected = Function.prototype
) {
if (this.status === "fulfilled") {
onfulfilled(this.resolveVal);
}
if (this.status === "rejected") {
onrejected(this.rejectVal);
}
};
这样一来,promise 实例的状态只会变更一次,满足我们的需求了。
四、我可是解决异步问题的
目前为止,我们的实现有一个最大的问题,promise 是用来解决异步问题的,但是目前的代码都是同步执行的,貌似缺少了最关键的逻辑。
如果尝试使用我们的 Promise 做点什么就会发现问题:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("value");
}, 3000);
});
promise.then(data => {
console.log(data);
});
正常来讲,代码的执行结果应该为,3秒后打印出“value”,但是结果是并未输出任何信息。
如果你读过我之前的异步笔记,就会很容易的发现问题,通过 new 去调用 Promise 构造函数的时候,在 setTimeout 中调用了 resolve 方法。也就是说3秒后才去调用的 resolve 方法,并且在此时才去更改的 status 的状态。并且我们所实现的 then 方法中的 onfulfilled 和 onrejected 也完全是同步的,相当于 onfulfilled 在执行的时候,this.status 的值依然是 pending ,等到3秒后状态更新了,但是 onfulfilled 也早已执行完毕了。
解决这个问题的关键在于,我们要在合适的时间再去调用 onfulfilled 方法,这就需要在 then 方法中做一些修改,在执行到 then 的时候,如果 status 是 pending,那么就将用户传进来的 onfulfilled 和 onrejected 保存起来,等到 resolve 或者 reject的时候在拿出来执行。
修改代码如下:
function Promise(excutor) {
this.status = "pending";
this.resolveVal = null;
this.rejectVal = null;
// 用于保存 onfulfilled 、onrejected 方法
this.onFulfilledFunc = Function.prototype;
this.onRejectedFunc = Function.prototype;
const resolve = value => {
if (this.status === "pending") {
this.resolveVal = value;
this.onFulfilledFunc(this.resolveVal);
this.status = "fulfilled";
}
};
const reject = error => {
if (this.status === "pending") {
this.rejectVal = error;
this.onRejectedFunc(this.rejectVal);
this.status = "rejected";
}
};
excutor(resolve, reject);
}
Promise.prototype.then = function(
onfulfilled = Function.prototype,
onrejected = Function.prototype
) {
if (this.status === "fulfilled") {
onfulfilled(this.resolveVal);
}
if (this.status === "rejected") {
onrejected(this.rejectVal);
}
// 保存 onfulfilled 、onrejected 方法
if (this.status === "pending") {
this.onFulfilledFunc = onfulfilled;
this.onRejectedFunc = onrejected;
}
};
再执行上面的测试代码,可以看到3秒后打印出 “value”,测试通过,我们实现的 Promise 可以支持异步了。
五、我不仅支持异步,我自身也是异步的
Promise 不仅支持异步,并且它的成功或失败的回调也要异步执行,用一段代码测试下我们实现的 Promise。
let promise = new Promise((resolve, reject) => {
resolve("value");
});
promise.then(data => {
console.log(data);
});
console.log("1");
根据我们所知的情况,应该先输出 1 ,然后再输出 value。但执行后发现是先输出 value,然后再输出 1。
这样一来,promise 的回调就是同步执行的了,我们需要做的是将 resolve 和 reject 放到任务队列中执行。
这里最严谨的做法是,为了保证 Promise 属于 microtasks,很多 Promise 的实现库用了 MutationObserver 来模仿 nextTick。
我们偷个懒,先使用不那么严谨的 setTimeout 实现这个效果。将 resolve 和 reject 方法里的内容用 setTimeout 包裹起来即可。
六、细节完善
又经过我的一番测试,发现了几个小问题,逐一修复。
- 比如在 promise 实例状态变更之前,添加了多个 then 方法,那么第二个 then 中的 onFulfilledFunc 会覆盖第一个 then 中的 onFulfilledFunc。
解决办法:将所有 then 方法中的 onFulfilledFunc 储存为一个数组 onFulfilledArray,在 resolve 时,依次执行即可。
- 如果在构造函数中出错,promise 实例应该将状态更改为 rejected。
解决办法:try…catch 块对 executor 进行包裹。
到此为止,简易版的 Promise 就实现了,附上完整代码:
function Promise(excutor) {
this.status = "pending";
this.resolveVal = null;
this.rejectVal = null;
this.onFulfilledFuncArray = [];
this.onRejectedFuncArray = [];
const resolve = value => {
setTimeout(() => {
if (this.status === "pending") {
this.resolveVal = value;
this.status = "fulfilled";
this.onFulfilledFuncArray.forEach(fn => {
fn(this.resolveVal);
});
}
}, 0);
};
const reject = error => {
setTimeout(() => {
if (this.status === "pending") {
this.rejectVal = error;
this.status = "rejected";
this.onRejectedFuncArray.forEach(fn => {
fn(this.rejectVal);
});
}
}, 0);
};
try {
excutor(resolve, reject);
} catch (error) {
reject(error);
}
}
Promise.prototype.then = function(
onfulfilled = Function.prototype,
onrejected = Function.prototype
) {
if (this.status === "fulfilled") {
onfulfilled(this.resolveVal);
}
if (this.status === "rejected") {
onrejected(this.rejectVal);
}
if (this.status === "pending") {
this.onFulfilledFuncArray.push(onfulfilled);
this.onRejectedFuncArray.push(onrejected);
}
};
下一篇《如何从无到有实现Promise(下)》中将会继续实现 Promise ,完善我们的 Promise 并为它添加静态方法。