Promise解决了什么问题?
这篇文章假定您已经有了些许的Promise使用经验。关于Promise解决的问题,大多数文章值提出了“回调地狱”这一观点
这确实是Promise解决的一大问题,但是,关于Promise的亮点,还包括但不限于以下几方面:
-
解决回调函数的控制反转导致的信任问题:
Promise提供了针对于第三方在调用回调函数时关于错误的调用时机,错误的调用次数等方面的解决方案,具体实现可以参考《你不知道的Javascript》中卷 第二部分–第三章–3.3部分–Promise信任问题 相关内容。
-
指定回调函数的方式更加灵活:
之前的异步方式必须要在启动之前指定回调函数,这是因为以前的异步方式无法保存异步状态,导致会在得到结果后就立马进入回调函数中进行处理,而Promise因为可以保存异步的执行状态和返回值,所以无论是在异步启动前,还是启动后,甚至在已经得到结果后再指定回调函数都是被允许的。
宏队列与微队列
Event Loop(事件循环)是JavaScript的执行模型,不过它并不是我们这篇文章要探讨的,我们要引用其中两个很重要的两个概念:Macrotask(宏队列)、Microtask(微队列),理解了这两个概念,可以解决我们在编写Promise中的一些疑惑。
首先先来看三段代码:
// 第一段代码
setTimeout(() => {
console.log(1); // 我后输出
}, 0)
console.log(2); // 我先输出
// 第二段代码
Promise.resolve(1).then(resolved => console.log(resolved)); // 我后输出
console.log(2); // 我先输出
// 第三段代码
setTimeout(() => {
console.log(1); // 我第二个输出
Promise.resolve(3).then((e) => console.log(e)); // 我第3个输出
}, 0);
setTimeout(() => {
console.log(2); // 我最后输出
}, 0);
console.log(4); // 我先输出
从前面两段代码中,我们不难看出,setTimeout和
Promise.prototype.then都会异步执行其中的代码片段,但是第三段代码中,在最后加入异步队列的
Promise.prototype.then却排在了第二个
setTimeout`前面。
我们直接说结论,原因是在JavaScript的执行模型中,异步队列分为宏队列和微队列两种,其中setTimeout
属于宏队列,Promise.prototype.then
属于微队列。二者的共同点是都会等待JS执行栈全部pop空后才执行。
优先级也就是说
调用栈 > 微任务 > 宏任务
它们的区别是宏队列一次只会弹出一个回调函数执行,并且每一个宏队列函数执行完毕后,都会检测当前微队列中有无待执行函数,如果有会一次性将微队列中的全部待执行函数执行完毕。
带入到第三段代码中,Promise之所以会先于第二个定时器就是因为在第一个定时器执行完毕后,检测到微队列中包含一个Promise待执行函数,所以会将微队列的函数执行完后,再返回宏队列执行接下来的代码。
开始实现Promise
我们先看一段基本的Promise使用代码:
let asyncCode = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
});
});
asyncCode.then(
(resolved) => {
console.log(resolved); // 1
},
(rejected) => {
console.log(rejected);
}
);
我们先列出从上面的代码中就可以看出的规范:
- Promise是一个构造函数,构造一个Promise实例需要传入一个回调函数;
- 传入的参数函数中包含两个参数,并且这两个参数也是函数;
- 构造出的Promise实例身上包含
then
方法; then
方法中需要传入两个函数类型的参数,两个函数各有一个参数。
以上面的规则,我们就可以开始构建:
(function (window) {
function MyPromise(executor) {
function resolve(value) {
}
function reject(reason) {
}
executor(resolve, reject)
}
MyPromise.prototype.then = function (onResolved, onRejected) {
}
})(window)
术语
- 解决(fulfill):指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用
fulfill
来表示解决,但在后世的 promise 实现多以resolve
来指代之。 - 拒绝(reject):指一个 promise 失败时进行的一系列操作。
- 终值(eventual value):所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)。
- 据因(reason):也就是拒绝原因,指在 promise 被拒绝时传递给拒绝回调的值。
Promise构造函数编写
根据规范,一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。
等待态(Pending)
处于等待态时,promise 需满足以下条件:
- 可以迁移至执行态或拒绝态
执行态(Fulfilled)
处于执行态时,promise 需满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的终值
拒绝态(Rejected)
处于拒绝态时,promise 需满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的拒因
这里的不可变指的是恒等(即可用 ===
判断相等),而不是意味着更深层次的不可变(盖指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。
另外,ES2015中并没有选择”Fulfilled“作为执行态,而是选择与”rejected“相对应的“resolved”,我们的实现也遵从ES2015的实现。
function MyPromise(executor) {
// Promise当前的状态
this.state = "pending";
// 由于Promise只会有一种状态,所以我们利用一个属性来存储返回的终值或拒因
this.data = undefined;
// 使用数组是因为同一个Promise实例可能被多次调用then方法
// 每个元素的结构:{onResolved() {}, onRejected() {}}
this.callbacks = [];
// ... other code
}
接下来我们再来实现需要传入executor
中的resolve
和reject
方法,它们的逻辑包括:
- 由于Promise只有一次更改状态的机会,所以只要当前的
state
不为"pending"
,直接return; resolve
需要保存本次异步的终值,reject
需要保存本次异步的拒因;- 由于浏览器并没有开放将代码push到微队列的接口,所以我们借用官方的
Promise.prototype.then
方法来实现合适的回调函数的调用时机
function MyPromise(executor) {
// ... other code
function resolve(value) {
if (this.state !== "pending") return;
// 将状态改为resolved
this.state = "resolved";
// 保存value数据
this.data = value;
// 如果有待执行的回调函数,依次添加入异步队列中(此处用宏队列模拟微队列)
if (this.callbacks.length > 0) {
let _this = this;
Promise.resolve(null).then((e) => {
_this.callbacks.forEach((callbacksObj) => {
callbacksObj.onResolved(value);
});
});
}
}
function reject(reason) {
if (this.state !== "pending") return;
// 将状态改为rejected
this.state = "rejected";
// 保存reason数据
this.data = reason;
// 如果有待执行的回调函数,依次添加入异步队列中(此处用宏队列模拟微队列)
if (this.callbacks.length > 0) {
// setTimeout(() => {
// this.callbacks.forEach((callbacksObj) => {
// callbacksObj.onRejected(reason);
// });
// });
// 使用Promise.prototype.then来模拟为微队列效果
Promise.resolve(null).then((e) => {
this.callbacks.forEach((callbacksObj) => {
callbacksObj.onRejected(reason);
});
});
}
}
}
最后因为Promise的构建是同步执行的,所以我们在构造函数中立即执行传进来的构建器:
// 立即执行executor
try {
executor(resolve.bind(this), reject.bind(this));
} catch (error) {
reject(error); // 如果执行器抛出异常,Promise为失败状态
}
构造函数的最终代码为
(function (window) {
function MyPromise(executor) {
// Promise当前的状态
this.state = "pending";
// 由于Promise只会有一种状态,所以我们利用一个属性来存储返回的终值或拒因
this.data = undefined;
// 使用数组是因为同一个Promise实例可能被多次调用then方法
// 每个元素的结构:{onResolved() {}, onRejected() {}}
this.callbacks = [];
function resolve(value) {
if (this.state !== "pending") return;
// 将状态改为resolved
this.state = "resolved";
// 保存value数据
this.data = value;
// 如果有待执行的回调函数,依次添加入异步队列中(此处用宏队列模拟微队列)
if (this.callbacks.length > 0) {
let _this = this;
Promise.resolve(null).then((e) => {
_this.callbacks.forEach((callbacksObj) => {
callbacksObj.onResolved(value);
});
});
}
}
function reject(reason) {
if (this.state !== "pending") return;
// 将状态改为rejected
this.state = "rejected";
// 保存reason数据
this.data = reason;
// 如果有待执行的回调函数,依次添加入异步队列中(此处用宏队列模拟微队列)
if (this.callbacks.length > 0) {
// setTimeout(() => {
// this.callbacks.forEach((callbacksObj) => {
// callbacksObj.onRejected(reason);
// });
// });
// 使用Promise.prototype.then来模拟为微队列效果
Promise.resolve(null).then((e) => {
this.callbacks.forEach((callbacksObj) => {
callbacksObj.onRejected(reason);
});
});
}
}
// 立即执行executor
try {
executor(resolve.bind(this), reject.bind(this));
} catch (error) {
reject(error); // 如果执行器抛出异常,Promise为失败状态
}
}
// 用于测试,暂时假定Promise的状态为pending
MyPromise.prototype.then = function (onResolved, onRejected) {
this.callbacks.push({ onResolved, onRejected });
};
window.MyPromise = MyPromise;
})(window);