Promise是什么?
Promise对我来说,是一个当年非常困扰的知识点。在不用Promise的情况下,我好像也能通过传统的回调解决Promise能解决的所有问题,但我去搜索“Promise和普通回调有什么区别”的时候,答案总是告诉我:Promise可以通过链式调用避免陷入“回调地狱”的问题。
但这个问题其实对我的日常开发并没有构成非常大的影响,所以在很长时间中,我都没有动力把Promise语法和使用给学会。
途中也看过很多“带你手写Promise源码”的文章,总是看的一知半解:跟着文章把源码敲完了,仿佛能看懂每一行代码,也确实能运行起来,但就是不懂为什么要这么写。但是真正学会后,不得不说Promise用起来是真爽,语法简单易理解,代码简洁不冗余,代码质量一下上升了不少。
Promise的理解难点在我看来,是使用思路上有一点绕,使用方式和传统的“回调函数”有一些不同;涉及到的方法和属性又比较多一点,又是resolve
和reject
啦,又是fulfilled
和rejected
啦,又是.then
啦,又是.then
中传递的回调函数啦,它们之间还有一环套一环的逻辑关系。
但是别担心!这篇文章看完,包你“不仅学会Promise怎么用,还能学会Promise如何实现”!全程无痛!面试工作都能用!
Promise怎么用?
我们先来看看,用传统的ajax发送请求是怎么发送的:
// 发送请求
$.ajax({
url: 'https://api.example.com/create',
data: {
name: 'John',
age: 30
},
success: function (data) {
console.log('请求成功', data);
},
error: function (error) {
console.log('请求失败', error);
}
});
看起来也很简洁,但如果我们需要在请求成功后再通过请求结果的值发送新的请求,代码就会成为这样:
// 发送请求
$.ajax({
url: 'https://api.example.com/create',
data: {
name: 'John',
age: 30
},
success: function (data) {
console.log('请求成功', data);
$.ajax({
url: 'https://api.example.com/query',
data: data,
success: function (data) {
console.log('请求成功', data);
},
error: function (error) {
console.log('请求失败', error);
}
});
},
error: function (error) {
console.log('请求失败', error);
}
});
此时代码就出现了嵌套,实际的项目中可能会有多级的嵌套请求,在代码中就会出现多级嵌套,这样的代码维护起来简直要命。
于是Promise出现了。我们如果用Promise来改造一下ajax,代码就会成为这样:
ajaxPromise("https://api.example.com/create", {
name: "John",
age: 30,
})
.then(function (data) {
return ajaxPromise("https://api.example.com/query", data);
})
.catch(function (error) {
console.log("请求失败", error);
});
怎么样,是不是代码一下就简洁了很多,就算我们还要再循环嵌套多个请求,也无非就是:
ajaxPromise("https://api.example.com/create", {
name: "John",
age: 30,
})
.then(function (data) {
return ajaxPromise("https://api.example.com/query", data);
})
.then(function (data) {
return ajaxPromise("https://api.example.com/query1", data);
})
.then(function (data) {
return ajaxPromise("https://api.example.com/query2", data);
})
.catch(function (error) {
console.log("请求失败", error);
});
依然有高可读性和高维护性,不会出现任何多余的代码嵌套。
那么,这个ajaxPromise是如何实现的呢?代码其实也很简单:
function ajaxPromise(url, data) {
return new Promise((resolve, reject) => {
$.ajax({
url: url,
data: data,
success: function (data) {
resolve(data);
},
error: function (error) {
reject(error);
},
});
});
}
现在看不懂也没关系,我们把下面的初级版源码学完就懂啦。
Promise初级版源码
Promise基本功能
我们先来说说,如何实现一个只能实现基本功能的Promise源码:
首先使用Promise的第一步,就是新建一个Promise对象,并且要传入一个方法作为参数。类似这样:
new Promise(() => {
...
});
对于Promise来说,接收的方法要立即执行。所以我们要先新建一个Promise类,并将接收的一个参数方法立即执行。
我们用ES6的class
关键字来定义一个类,把自定义方法以executor
进行命名,并在类的构造函数constructor
中进行接收调用:
class Promise {
constructor(executor) {
executor(); // 立即执行参数方法
}
}
随后我们要知道,一个promise对象需要有哪些属性?我们来看看promise对象长啥样:
可以看到,图上的三个不同的Promise实例对象,它们都有两个共同属性值:PromiseState
和PromiseResult
,这就是我们下一步要定义到到Promise
中的值。
我们要先介绍一下第一个属性值:PromiseState
,也就是Promise的“状态”:每个Promise对象都会有一个“状态”,“状态”无非上图三种情况:
- pending(等待状态)
- fulfilled(成功状态)
- rejected(失败状态)
每一个Promise对象在初始阶段都是pending状态,也就是“等待执行完成”的状态,而随着我们的自定义方法executor
的执行,根据方法执行结果来对这个Promise对象的状态PromiseState
进行相应修改。
另一个属性值PromiseResult
是Promise对象的值,每个promise都有自己的值,这个值可以是任何类型,默认是undefined
。当然这个值和“状态”一样,都是在参数方法executor
执行时被修改的。
我们来在代码上定义这两个属性值的初始状态:
class Promise {
constructor(executor) {
const self = this;
self.state = "pending";
self.result = undefined;
executor();
}
}
完成了对promise对象基本属性的定义,我们要来做关键一步了:刚刚一直在说promise对象的状态和值都是被参数方法executor
所改变的,我们就来看看自定义方法executor
是怎么改变promise对象的状态和值的。
我们先来想想,Promise到底干了个啥?举个例子:Promise是一个很靠谱的人,他可以完成你交给他的所有事情,你只用事先把要办事的具体流程单填好(executor):
除了告诉它你要办的事是啥,还有一个很重要的特点:Promise办事,并不是只把你交代的事情“做完”就算完事,而是“你们事先说好怎样算成功,怎样算失败”(比如上图例子,偷东西并不算成功,而是东西要符合条件才算成功)。只要你在流程单上,把什么是“办成了”的状态用绿色的笔打个勾,把什么是“办垮了”用红笔打个叉。
Promise并不会以事情做没做完来决定这一单是否成功,而是以你定义的方式来决定。最后不管事情结果咋样,Promise都会告诉你结果。
所以回到代码上,你传入的executor
参数就是流程单,你在里面写上要交给Promise执行的代码,并且在其中调用resolve
方法表示这事成功了,而调用reject
方法表示这事失败了。比如这样:
new Promise((resolve, reject) => {
const 采购清单 = 偷东西()
if (采购清单.include('海鱼')) {
resolve() // 绿笔打的勾,说明采购清单中的采购物品是海鱼,任务成功
} else if (采购清单.include('河鱼')) {
reject() // 红笔打的叉,说明采购清单中的采购物品是河鱼,任务失败
}
})
这下你应该明白Promise是怎么用的了吧!至于这个resolve
和reject
是从哪来的?当然是源码中定义的,你就当它们是Promise这个人给你准备的绿笔和红笔,方便你写流程单用的就好。
这两个方法做的事情其实并不神秘,相信有同学已经猜到了,这两个方法只不过是用来改变promise状态和值而已。我们来简单的实现一下:
class Promise {
constructor(executor) {
const self = this;
self.state = "pending";
self.result = undefined;
executor(
// 在调用自定义方法executor时,传入两个方法供executor调用
function resolve(value) {
self.state = 'fulfilled';
self.result = value;
},
function reject(reason) {
self.state = "rejected";
self.result = reason;
}
);
}
}
其实不过就是这样,Promise在调用你传入的函数executor
时,传入对应的resolve
和reject
方法作为参数,方法功能就是改变当前promise对象的state
和result
:
- 如果你调用了resolve,则表示任务成功,promise的状态会变为fulfilled;
- 如果你调用了reject,则表示任务失败,promise的状态会变为rejected;
- 不管你调用哪个方法,你调用方法时传递的参数会成为promise的result的值;
我们来调用试试看:
const shoppingList = ['海鱼', '海鱼', '海鱼']; // 采购清单
const steal = () => { // 偷采购清单的方法
return shoppingList;
}
var p = new Promise((resolve, reject) => {
const list = steal()
if (list.includes('海鱼')) {
resolve(`任务成功,购买清单中有${list.filter(item => item === '海鱼').length}份海鱼`)
} else if (list.includes('河鱼')) {
reject(`任务成功,购买清单中有${list.filter(item => item === '河鱼').length}份河鱼`)
}
});
console.log(p);
结果看起来已经很像那么回事了,Promise已经帮我们把交给他的任务顺利完成,并且把任务状态和结果都修改成了正确的样子。
让我们再来完善一下代码,Promise有个特点:Promise的状态一旦被确定就不能再被改变了。所以我们把resolve
和reject
方法的功能抽到一个change
方法中并进行判断。这个change
方法后面还有大用处,我们多看它几眼:
class Promise {
constructor(executor) {
self = this;
self.state = "pending";
self.result = undefined;
// 新建change方法来管理promise的状态和值
const change = function change(state, data) {
if (self.state !== "pending") return; // 判断是否已为确定状态(fulfilled, rejected)
self.state = state;
self.result = data;
};
executor(
function resolve(value) {
change("fulfilled", value);
},
function reject(reason) {
change("rejected", reason);
}
);
}
}
我们来试试“防止二次篡改状态”的逻辑是否生效:先调用resolve
方法再来调用reject
方法会怎么样:
var p = new Promise((resolve, reject) => {
resolve('成功了');
reject('失败了');
});
console.log(p);
没问题,后面调用的reject并没有再次改变promise的状态!
好了,Promise本体部分已经接近完成了,我们接下来又有新问题了:这偷采购清单可是件大事,任务成功后,怎么都得开个庆功宴;但如果失败了,我们就要开复盘大会。
而Promise不仅可以帮你完成任务本身,你还可以把要在任务结束后要做的事情也交代给他,他一样会帮你实现。
promise.then
用过Promise的同学一定知道我说的是.then
方法了。
.then
方法就像你交代Promise的第二张办事流程单,你要在上面写好任务成功后做什么、任务失败后做什么:
所以.then
方法的调用其实非常简单,就是传入两个参数,分别对应成功后的方法和失败后的方法:
p.then(
(data) => {
// 成功方法
},
(reason) => {
// 失败方法
}
);
.then
方法的实现也很简单,我们在其中判断当前promise的状态,如果是fulfilled
就执行第一个参数方法,如果是rejected
就执行第二个参数方法。并且把当前promise的值(result)作为参数传递给调用的参数方法。
而.then
方法是每个promise对象都能调用的方法,所以我们把它定义在Promise的原型对象上:
Promise.prototype.then= function then(onfulfilled, onrejected) {
if (this.state === "fulfilled") {
onfulfilled(this.result);
} else if (this.state === "rejected") {
onrejected(this.result);
}
}
很好理解吧!我们来试试效果:
var p = new Promise((resolve, reject) => {
const list = steal();
if (list.includes("海鱼")) {
resolve(list.filter((item) => item === "海鱼").length);
} else if (list.includes("河鱼")) {
reject(list.filter((item) => item === "河鱼").length);
}
});
p.then(
(data) => {
console.log(`任务成功,开庆功宴,开了${data}瓶香槟`);
},
(reason) => {
console.log(`任务失败,开庆功宴,抽了${reason}个嘴巴`);
}
);
结果如我们所愿,promise把我们在.then
中传递的成功方法顺利调用了,你也可以试试如果我们把任务清单内容改成“河鱼”,这里的结果会不会有变化呢?
用Promise处理异步问题
但我们现在的代码有点问题:一般我们需要交给Promise的任务都不是可以马上完成的(同步任务),而是需要等待一段时间才可以完成的(异步任务)。
我们假设“偷购物清单”这件事需要一秒钟,我们再来看看会怎么样:
var p = new Promise((resolve, reject) => {
console.log('开始执行任务,一秒后执行完毕...');
setTimeout(() => { // 设定定时器,一秒后偷到购物清单
const list = steal();
if (list.includes("海鱼")) {
resolve(list.filter((item) => item === "海鱼").length);
} else if (list.includes("河鱼")) {
reject(list.filter((item) => item === "河鱼").length);
}
}, 1000)
});
p.then(
(data) => {
// 成功方法
console.log(`任务成功,开庆功宴,开了${data}瓶香槟`);
},
(reason) => {
// 失败方法
console.log(`任务失败,开庆功宴,抽了${reason}个嘴巴`);
}
);
这时候我们的代码就出问题了,我们没有获得任何输出结果。
原因也不复杂:.then
中的方法是立即执行的,此时p
这个promise还没有确定任务结果(还没有执行完任务),在then
方法执行时p
还是pending状态,既不属于成功(fulfilled)的,也不属于失败(rejected)的,自然就不会有任何输出结果了。那这个多简单!我们再写一个判断条件不就行了:
Promise.prototype.then= function then(onfulfilled, onrejected) {
if (this.state === "fulfilled") {
onfulfilled(this.result);
} else if (this.state === "rejected") {
onrejected(this.result);
} else if (this.state === "pending") {
// 还未完成任务的情况
console.log(this);
}
}
这下有点不好办了,then
方法在被执行的时候任务还没有执行完,此时我们也不知道任务结果,这也没法去调用相应的回调函数呀。
我们想想怎么解决这个问题。还记得当promise任务执行完成,不论成功失败,唯一可以改变promise状态的方法是什么吗?没错,就是我们前面写的change
方法:
class Promise {
constructor(executor) {
self = this;
self.state = "pending";
self.result = undefined;
// 新建change方法来管理promise的状态和值
const change = function change(state, data) {
if (self.state !== "pending") return;
self.state = state;
self.result = data;
};
executor(
(data) => {
//resolve
change("fulfilled", data);
},
(reason) => {
// reject
change("rejected", reason);
}
);
}
}
既然只有change
方法可以改变promise的状态,也就是说不管任务什么时候执行完毕,change
方法都是第一个知道的。那我们不如这样:如果then
方法执行时promise还是pending状态(还没有执行完任务),那就麻烦change
方法帮帮忙,等任务执行完后,由change
方法来调用对应的成功/失败回调。
逻辑很简单,但还是有个小问题:then
方法和change
方法直接没法直接沟通,毕竟then
方法是原型方法,而change
方法是个私有方法。我们不如直接给每个任务都建立一个备忘录,then
方法如果遇到当前promise还没完成的情况,就把它所接收到的两个函数记录在备忘录上;而change
方法执行的时候,就去看看备忘录上有没有记录,有的话就把对应的方法执行了。
说干就干:
class Promise {
constructor(executor) {
self = this;
self.state = "pending";
self.result = undefined;
self.onfulfilledCallbacks = []; // 成功回调备忘录
self.onrejectedCallbacks = []; // 失败回调备忘录
const change = function change(state, data) {
if (self.state !== "pending") return;
self.state = state;
self.result = data;
// 执行“备忘录”中的回调方法
const callbacks = self.state === "fulfilled" ? self.onfulfilledCallbacks : self.onrejectedCallbacks; // 根据成功/失败状态,判断要执行的备忘录
callbacks.forEach((callback) => { // 遍历执行备忘录中所有方法
callback(self.result);
});
};
executor(
(data) => {
change("fulfilled", data); //resolve
},
(reason) => {
change("rejected", reason); // reject
}
);
}
}
Promise.prototype.then= function then(onfulfilled, onrejected) {
if (this.state === "fulfilled") {
onfulfilled(this.result);
} else if (this.state === "rejected") {
onrejected(this.result);
} else if (this.state === "pending") {
// 还未完成任务的情况
this.onfulfilledCallbacks.push(onfulfilled); // 将当前未执行的成功回调写到“成功备忘录”中
this.onrejectedCallbacks.push(onrejected); // 将当前未执行的失败回调写到“失败备忘录”中
}
}
新增的代码不多,我们将“备忘录”设置为两个数组:“成功备忘录(onfulfilledCallbacks)”和“失败备忘录(onrejectedCallbacks)”。因为同一个Promise对象的then
方法可以被多次调用(可以多开几次庆功宴),所以我们用两个数组来进行存储。
在then
方法中,对于当前未完成的promise,我们将对应的回调方法存入到对应的备忘录中。
在change
方法中,我们根据promise执行完成后的情况,对应地遍历执行对应的备忘录中的回调方法。
就是这么简单,我们来看看效果:
完美实现了!
完整版初级Promise
当然,Promise还有一个特点就是:executor
会同步执行,而.then
中的回调方法都是异步执行的。我们来略微改造一下代码,把每个.then
中的回调方法都放入到异步微任务队列中去等待执行(如果你不知道什么是异步宏任务和异步微任务,记得留言告诉我,下次教你浏览器的“事件循环机制”),我们来看看改造后的代码全貌:
class Promise {
constructor(executor) {
self = this;
self.state = "pending";
self.result = undefined;
self.onfulfilledCallbacks = []; // 成功回调备忘录
self.onrejectedCallbacks = []; // 失败回调备忘录
const change = function change(state, data) {
if (self.state !== "pending") return;
self.state = state;
self.result = data;
// 执行“备忘录”中的回调方法
const callbacks = self.state === "fulfilled" ? self.onfulfilledCallbacks : self.onrejectedCallbacks; // 根据成功/失败状态,判断要执行的备忘录
callbacks.forEach((callback) => { // 遍历执行备忘录中所有方法
queueMicrotask(() => callback(self.result));
});
};
executor(
(data) => {
change("fulfilled", data); //resolve
},
(reason) => {
change("rejected", reason); // reject
}
);
}
}
Promise.prototype.then= function then(onfulfilled, onrejected) {
if (this.state === "fulfilled") {
queueMicrotask(() => onfulfilled(this.result));
} else if (this.state === "rejected") {
queueMicrotask(() => onrejected(this.result));
} else if (this.state === "pending") {
// 还未完成任务的情况
this.onfulfilledCallbacks.push(onfulfilled); // 将当前未执行的成功回调写到“成功备忘录”中
this.onrejectedCallbacks.push(onrejected); // 将当前未执行的失败回调写到“失败备忘录”中
}
}
const shoppingList = ["海鱼", "海鱼", "海鱼"];
const steal = () => {
return shoppingList;
};
var p = new Promise((resolve, reject) => {
console.log('开始执行任务,一秒后执行完毕...');
setTimeout(() => { // 设定定时器,一秒后偷到购物清单
const list = steal();
if (list.includes("海鱼")) {
resolve(list.filter((item) => item === "海鱼").length);
} else if (list.includes("河鱼")) {
reject(list.filter((item) => item === "河鱼").length);
}
}, 1000)
});
p.then(
(data) => {
// 成功方法
console.log(`任务成功,开庆功宴,开了${data}瓶香槟`);
},
(reason) => {
// 失败方法
console.log(`任务失败,开庆功宴,抽了${reason}个嘴巴`);
}
);
后记
其实Promise的基本逻辑就是这样了,虽然我们写的这确实是个非常基础的版本,但你学完这篇文章的内容后,对于Promise的基本使用和基本实现逻辑一定是了然于胸啦。
你也可以用现在的这个Promise代码来尝试实现一下实际开发中会频繁用到的网络请求,尝试像第二章一样把ajax封装成一个ajaxPromise
方法等等。记得一定要把代码敲出来!看过和敲过的效果可是完全不同的!
下一篇文章,带你写一个符合A+规范的Promise,我们还要实现链式调用(让then
方法返回promise)、错误处理、Promise.all
等等功能!
写文不易,想看的同学多多留言点赞!