1. promise之前
回调函数是我们经常使用的,我们可以在回调函数中去处理我们得到某些数据之后的一些操作,但是如果回调函数嵌套的太多,那可能就头痛了,例如下面的示例:
function show () {
fn(1);
setTimeout(() => {
fn(2)
setTimeout(() => {
fn(3)
setTimeout(() => {
fn(4)
setTimeout(() => {
fn(5)
},100)
},100)
},100)
},100)
在这个实例中我们想要模拟的场景类似于请求嵌套,这样是没有问题的,但是我们需要去排查问题的时候就会发现,这里面的耦合度太高了,嵌套的太深,以至于我们需要一层一层的去抽丝剥茧才能找到我们需要的作用域。这就是我们所谓的回调地狱。回调本来没问题,但问题是嵌套的太深,就会增加阅读的难度。
那有没有一种方式能更轻松的查看代码,查看逻辑之间的关系喃?这时候Promise的出现就成功的解决了这个问题。
2. 什么是promise?
Promise是异步编程的一种解决方案,它是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。Promise遵循Promise/A+规范,这个规范里面规定了promise需要遵循的规则。
2-1 promise的用法
let p1 = new Promise((resolve,reject)=> {
resolve(1)
})
p1.then((result) => {
console.log(result)
},err => {
console.log(error)
})
这是一个promise的简单使用,示例可以看出Promise是一个类,通过new调用,并且接收一个回调函数,回调函数接收两个函数作为参数,根据A+规范可以知道,resolve是返回成功的值,而reject则是将错误拦截。Promise会返回一个实例对象,这个对象有then方法。
2-2 基本规则
知道了promise的使用,我们再来看看promise的基本规则。 刚刚我们说了Promise/A+规范,那么我们具体来看看这个规范有哪些规则。
- promise拥有三个状态,分别是pending、fulfiled、reject,这三个状态的转换过程是pending->fulfiled,pending->reject。从这个规范可以看出状态的改变只能从pending开始,一旦变化就不可再逆;
- fulfiled状态下拥有一个不可变的值value;
- reject状态下拥有一个不可变的错误信息error;
- Promise在new的时候,接收一个函数,这个函数有两个参数,resolve为成功,接收一个参数value,reject为失败,接收一个错误信息;
- Promise在new时接收的函数要立即执行;
3. 代码编写
通过以上的规则我们,我们可以得出一个Promise的基础形态:
3.1 Promise雏形
let PENDING = "pending", FULFILED = "fulfiled", REJECT = "reject";
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = null;
this.error = null;
let resolve = (value) => {}
let reject = (reason) => {}
try {
if (!executor) {
reject(new Error('必须传递一个函数'));
} else {
executor(resolve, reject);
}
} catch (e) {
reject(e);
}
}
}
目前我们看见的就是Promise类最基本的形态,它有状态值,成功后不可变的值,以及一个失败后不可变的拒绝原因,然后立即去执行我们传入的回调函数。
那么我们现在根据规则去给它添加一些简单的逻辑:
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = null;
this.error = null;
let resolve = (value) => {
// 根据规则,状态只能从pending开始
// 如果是resolve,则表示成功
if(this.status === PENDING) {
// 将状态修改为成功
this.status =FULFILED;
// 把值赋给成功时的变量
this.value = value;
}
}
let reject = (reason) => {
// 根据规则,状态只能从pending开始
// 如果是reject,则表示失败
if(this.status === PENDING) {
// 把状态修改为失败
this.status =REJECT;
// 把拒绝原因存起来
this.error = reason;
}
}
try{
if(!executor) {
reject(new Error('必须传递一个函数'));
}else{
executor(resolve,reject);
}
}catch(e){
reject(e);
}
}
then = () => {
}
catch = (fn) => {
fn(this.error)
}
}
这个时候其实我们就可以去创建一个promise实例了
const p = new MyPromise((resolve,reject) => {
resolve(1)
})
console.log(p)
在浏览器中打开,发现p实例上面的一些属性:
MyPromise {status: 'fulfiled', value: 1, error: null, then: ƒ, catch: ƒ}
catch: (fn) => { fn(this.error) }
error: null
status: "fulfiled"
then: () => { }
value: 1
[[Prototype]]: Object
这时候我们发现确实是按照写的发展,状态变成了“fulfiled”,并且值也记录到了,我们再尝试把resolve变成reject,在打印看看。
const p = new MyPromise((resolve,reject) => {
reject("错误信息")
})
console.log(p)
MyPromise {status: 'reject', value: null, error: '错误信息', then: ƒ, catch: ƒ}
catch: (fn) => { fn(this.error) }
error: "错误信息"
status: "reject"
then: () => { }
value: null
[Prototype]]: Object
状态也变成了“reject”,拒绝原因也存储起来了,看来我们前面的代码是没有问题的。
3.2 创建then的执行逻辑
这时候我们来完善then方法,因为promise的作用是采用链式调用的方法解决回调地狱给我们带来的一些麻烦。
class MyPromise {
constructor(executor) {
// 省略其他代码
}
// 省略其他代码
then = (onfulfiled,onrejected) => {
if(this.status === FULFILED) {
onfulfiled(this.value);
}
if(this.status === REJECT) {
onrejected(this.error);
}
}
}
这时候我们将刚刚的实例进行then操作,在浏览器执行看看会得到什么:
p.then(() => {
console.log(1)
},err => {
console.log(err)
}
}
// 把resolve(1)改成reject(“错误信息”)
p.then(() => {
console.log(1)
},err => {
console.log(err)
}
}
// 代码执行的结果
1
// 改成reject后
错误信息
这时候能在浏览器看见1,就说明then函数在resolve(1)时执行了onfulfiled函数,在reject("错误信息")时执行了onreject函数。
好了,代码编写到这步,我们已经基本上完成了promise的雏形,我们可以通过new得到一个promise实例,同时可以通过then链式调用一次。但是这好像并不能满足我们的要求,这时候我们只需要一步操作就可以发现我们现在的逻辑有缺陷。
const p = new MyPromise((resolve, reject) => {
// 我们把resolve函数放在一个setTimeout中执行
setTimeout(() => {
resolve(1)
})
})
p.then (res => {
console.log(1)
},err => {
console.log(err)
})
这时候可以在浏览器中发现其实它什么都没有打印,这就是我们所说的第一个缺陷。 出现这个问题的原因是resolve的执行顺序晚于then方法的执行,因为resolve是放在宏任务中执行的,它是在当前所有任务执行完后再执行,也就是说then方法执行时,状态依然为pending,这时候就不会触发onfulfiled方法,也不会出发onreject方法。 要解决这个问题也很容易,在then方法中添加一个判断,如果status=== “pending”,我们就把onfulfiled和onreject分别收集起来,只需要在resolve或者reject方法执行的时候再去执行我们传递的对应回调。
// 其他代码省略...
constructor(executor) {
// 其他代码省略...
// 新增
this.fulfilesCallbacks = [];
this.rejectCallback = [];
// 修改
let resolve = (value) => {
if(this.status === PENDING) {
this.status =FULFILED;
this.value = value;
this.fulfilesCallbacks.forEach(callback => callback());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status =REJECT;
this.error = reason;
this.rejectCallback.forEach(callback => callback());
}
}
}
then = (onfulfiled,onrejected) => {
if(this.status === FULFILED) {
onfulfiled(this.value);
}
if(this.status === REJECT) {
onrejected(this.error);
}
if(this.status === PENDING) {
this.fulfilesCallbacks.push(() => {
onfulfiled(this.value);
});
this.rejectCallback.push(() => {
onfulfiled(this.error);
});
}
}
// 修改后再执行
const p = new MyPromise((resolve, reject) => {
// 我们把resolve函数放在一个setTimeout中执行
setTimeout(() => {
resolve(1)
},1000)
})
p.then (res => {
console.log(1)
},err => {
console.log(err)
})
这时就可以发现,1秒后我们打印出了1,bingo!
如果就刚刚的代码中,我们直接使用链式调用,会发生什么?我们来试试
p.then (res => {
console.log(1)
},err => {
console.log(err)
}).then (res => {
console.log(1)
},err => {
console.log(err)
})
这时候我们有两个then进行链式调用,这就是我们期望把回调地狱改成的模样。打开浏览器的控制台:
test.html:385 Uncaught TypeError: Cannot read properties of undefined (reading 'then')at test.html:385:3(anonymous) @ test.html:385
test.html:382 1
报错了。这并不奇怪,因为我们第一个then执行完后什么都没返回,所以再用then方法就会报错。如果想要promise无限支持then的链式调用,那我们需要分析一下它应该具备什么属性。
- 它得有一个then方法
- 它得和第一个then拥有一样的功能
等等,这不就是说前一个then方法要返回一个promise吗?😏😏
为了达成链式,我们默认在第一个then里返回一个promise。规定了一种方法,就是在then里面返回一个新的promise,称为promise2:promise2 = new Promise((resolve, reject)=>{});
- 将这个promise2返回的值传递到下一个then中;
- 如果返回一个普通的值,则将普通的值传递给下一个then中;
当我们在第一个then中return了一个参数(参数未知,需判断)。这个return出来的新的promise就是onFulfilled()或onRejected()的值;
有了这个方法我们就继续完成我们的代码:
// 省略其他代码...
then (onfulfiled, onrejected) {
// 想要实现then的链式调用,并且可以多次then调用
// 思路: then函数就要继续返回Promise
/**
* 首先,要看x是不是promise。
*如果是promise,则取它的结果,作为新的promise2成功的结果
*如果是普通值,直接作为promise2成功的结果
*所以要比较x和promise2
*/
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILED) {
setTimeout(() => {
let x = onfulfiled(this.value);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
}
if (this.status === REJECT) {
setTimeout(() => {
let x = onrejected(this.error);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
}
if (this.status === PENDING) {
this.fulfilesCallbacks.push(() => {
setTimeout(() => {
let x = onfulfiled(this.value);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
});
this.rejectCallback.push(() => {
setTimeout(() => {
let x = onrejected(this.error);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
});
}
})
return promise2;
}
static resolvePromise (promise2, x, resolve, reject) {
if (promise2 === x) return new Error("不要返回promise本身,这样会造成重复调用的死循环");
let called = false;
if (x !== null && (typeof x === "object" || typeof x === "function")) {
try {
let then = x.then;
if (typeof then === "function") {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
error => {
if (called) return;
called = true;
reject(error); // 如果失败了就直接抛出错误
}
)
} else {
// 如果返回值是一个值,就直接传递给下一个promise
resolve(x)
}
} catch (error) {
if (called) return;
called = true;
reject(error); // 如果失败了就直接抛出错误
}
} else {
resolve(x)
}
}
这段代码中,我们封装了一个resolvePromise静态方法,这个方法接收四个参数,分别是要返回的promise2,一个return的值x,以及一个resolve和reject,这个方法主要是为了做以下几件事:
- 如果promise2和x相同,直接return,并抛出错误,因为这样会造成死循环;
- x 不能是null
- x 是普通值 直接resolve(x)
- x 是对象或者函数(默认promise),
let then = x.then声明了then - 如果取then报错,则走reject()
- 如果then是个函数,则用call执行then,第一个参数是this,后面是成功的回调和失败的回调
- 如果成功的回调还是pormise,就递归继续解析
- 成功和失败只能调用一个 所以设定一个called来防止多次调用
最后把promise2作为then的返回值返回回去,这样,then执行后我们得到的依然是一个promise,我们去控制台打印看看:
const p1 = p.then (res => {
console.log(1)
},err => {
console.log(err)
})
console.log(p1)
// 控制台输出
MyPromise {status: 'pending', value: null, error: null, fulfilesCallbacks: Array(0), rejectCallback: Array(0)}
error: null
fulfilesCallbacks: []
rejectCallback: []
status: "fulfiled"
value: undefined
[[Prototype]]: Object
bingo!到目前为止我们的promise基本上就算开发完成了,现在我们再来测试一下:
p.then (res => {
console.log(1)
},err => {
console.log(err)
}).then (res => {
console.log(2)
},err => {
console.log(err)
})
// 控制台输出
1
2
果然不出所料,浏览器打印出了1和2,现在我们的链式调用也完成了。
4. 完整代码
接下来我们把resolve,reject,all,race方法也实现一下,这个MyPromise类就算开发完成了(我直接把完整代码贴出来,这几个方法我作为MyPromise的静态方法)
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = null;
this.error = null;
this.fulfilesCallbacks = [];
this.rejectCallback = [];
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILED;
this.value = value;
this.fulfilesCallbacks.forEach(callback => callback(this.value));
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECT;
this.error = reason;
this.rejectCallback.forEach(callback => callback(this.error));
}
}
try {
if (!executor) {
reject(new Error('必须传递一个函数'));
} else {
executor(resolve, reject);
}
} catch (e) {
reject(e);
}
}
then (onfulfiled, onrejected) {
// 想要实现then的链式调用,并且可以多次then调用
// 思路: then函数就要继续返回Promise
/**
* 首先,要看x是不是promise。
*如果是promise,则取它的结果,作为新的promise2成功的结果
*如果是普通值,直接作为promise2成功的结果
*所以要比较x和promise2
*/
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILED) {
setTimeout(() => {
let x = onfulfiled(this.value);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
}
if (this.status === REJECT) {
setTimeout(() => {
let x = onrejected(this.error);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
}
if (this.status === PENDING) {
this.fulfilesCallbacks.push(() => {
setTimeout(() => {
let x = onfulfiled(this.value);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
});
this.rejectCallback.push(() => {
setTimeout(() => {
let x = onrejected(this.error);
MyPromise.resolvePromise(promise2, x, resolve, reject);
})
});
}
})
return promise2;
}
catch (fn) {
return this.then(null, fn)
}
static resolve (val) {
return new MyPromise((resolve, reject) => {
resolve(val);
})
}
static reject (error) {
return new MyPromise((resolve, reject) => {
reject(error); // 如果失败了就直接抛出错误
})
}
static race (promiseArr) {
return new MyPromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then(resolve, reject);
}
})
}
static all (promiseArr) {
let index = 0;
let resultList = [];
return new MyPromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then(data => {
resultList[index](data);
index++;
if (index === promiseArr.length) {
resolve(resultList)
}
}, reject).catch(err => {
reject(err);
});
}
})
}
static resolvePromise (promise2, x, resolve, reject) {
if (promise2 === x) return new Error("不要返回promise本身,这样会造成重复调用的死循环");
let called = false;
if (x !== null && (typeof x === "object" || typeof x === "function")) {
try {
let then = x.then;
if (typeof then === "function") {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
error => {
if (called) return;
called = true;
reject(error); // 如果失败了就直接抛出错误
}
)
} else {
// 如果返回值是一个值,就直接传递给下一个promise
resolve(x)
}
} catch (error) {
if (called) return;
called = true;
reject(error); // 如果失败了就直接抛出错误
}
} else {
resolve(x)
}
}
}
其实在开发Promise的时候,最难的点我认为是then的链式调用,这里需要搞清楚then的返回类型,以及如何做到链式调用。