主题
手写Promise,promise原理解析
一. 最基础的Promise
1. 基本特点:
- 我们用Promise时候是通过new Promise来使用的,因此可以用class来实现
- new Promise时候,要传入一个executor执行器函数,这个执行器函数是立即执行的
- 这个执行器函数接收两个参数,resolve,reject
- promise有三个状态,padding,fulfilled,rejected,当调用resolve时候,会将状态置为fulfilled,当调用reject时候,会将状态置为rejected,且只有当状态为padding时候才能让改变,状态一经改变就不能再变。
- 调用resolve或者reject时候,可以传入成功或者失败原因,不传默认为undefined;
- 执行器函数内也可以抛出异常,抛出异常时候,会将promise状态置为rejected;
- 每个promise都有一个then方法,then方法有两个参数,onFulfilled,onRejected;
- 当promise状态为成功时候,会调用then方法的第一个参数,并将成功的原因传递出去
- 当promise状态为失败时候,会调用then方法的第二个参数,并将失败的原因传递出去
2. 代码实现
以上就是最基本的promise的原理或特点,根据这些特点,可以得到一个基本的promise骨架
js
// 4. promise有三个状态
const STATUS = {
PADDING: "PADDING", // padding
FULFILLED: "FULFILLED", // fulfilled
REJECTED: "REJECTED", // rejected
};
class Promise {
// 2. 传入一个执行器函数
constructor(executor) {
// 4. 默认情况下,Promise的状态就是padding;
this.status = STATUS.PADDING;
this.value = undefined; // 5. 成功的原因,默认undefined,为了方便在then里面取到,所以实例上放一份
this.reason = undefined; // 5. 失败的原因,默认undefined,为了方便在then里面取到,所以实例上放一份
// 成功回调(注意使用箭头函数,否则theis会有问题)
const resolve = (val) => {
// 4. 只有padding时候才能改变状态
if (this.status === STATUS.PADDING) {
// 4. 调用resolve时候,将状态置位成功fulfilled
this.status = STATUS.FULFILLED;
// 5. 调用resoove时候,接收到成功的原因val
this.value = val;
}
};
// 失败回调(注意使用箭头函数,否则theis会有问题)
const reject = (reason) => {
// 4. 只有padding时候才能改变状态
if (this.status === STATUS.PADDING) {
// 4. 调用reject时,将promise置位失败,rejected
this.status = STATUS.REJECTED;
// 5. 调用reject时候, 接收到失败原因reason;
this.reason = reason;
}
};
// 6.可以在执行器函数内抛出异常,这里可以用try catch来捕获异常
try {
// 2. 执行器函数默认就会执行,
// 3.执行器函数接收个参数,分别是resolve,reject,都是函数
executor(resolve, reject);
} catch (e) {
// 6. 捕获到异常会导致promise失败
reject(e);
}
}
// 7. then是promise的实例方法,有两个参数,onFulfilled,onRejected
then(onFulfilled, onRejected) {
// 8. 若是当前promise的状态是成功则调用第一个参数onFulfilled
if (this.status === STATUS.FULFILLED) {
onFulfilled(this.value); // 8. 将成功的原因传递出去
}
// 9.若是当前promise的状态是失败则调用第二个参数onRejected
if (this.status === STATUS.REJECTED) {
onRejected(this.reason); // 9.将失败的原因传递出去
}
}
}
module.exports = Promise;
3. 测试
js
// 引入自己的promise
const Promise = require("./myPromise");
let p = new Promise((resolve, reject) => {
// throw new Error("抛出异常了");
// reject('出错了')
resolve('成功了')
});
p.then(
(res) => {
console.log("success:" + res);
},
(err) => {
console.log("fail:" + err);
}
);
本节githubCommit记录:promise基本骨架搭建
二. 改进:执行器内处理异步逻辑
1. 上一步存在的问题:
executor执行器里面不能执行异步方法,否则会得不到结果
例如:
js
// bad
const Promise = require("./myPromise");
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功了");
}, 1000);
});
p.then(
(res) => {
console.log("success:" + res);
// 没有输出,因为这里的then在执行的时候,promise里面的状态既不是fulfilled,也不是rejeted,
// 但是在then里面,我们没有对其他状态做判断,因此这里不会有输出
}
);
上面的例子,promise里面不能处理异步方法,只能处理同步。
2. 原因分析
我们在executor执行器函数内传入一个异步函数,执行器函数执行时,会将异步代码放入任务队列,等待执行,再调用then方法的时候,当前promise的状态可能还处于padding状态,但是在then方法里面,我们只处理了fulfilled和rejected状态下逻辑,因此then没有抛出结果,外面自然无法拿到结果。
3. 解决方案
- 发布订阅模式 executor里面传入异步方法时候,调用then方法,会进入padding状态,那么,此时可以将onFulfilled和onRejected方法缓存起来,当异步方法返回结果,会将promise状态改变,此时再根据状态将缓存起来的方法以此执行。
- 装饰模式,切片编程 我们在缓存onFulfilled和onRejected方法时候,可能还有其他额外逻辑,但是我们不能直接修改他们。因此可以用一个新的函数将其包装一下,并且加上额外的逻辑代码,不会改变onFulfilled和onRejected内部逻辑。
4. 代码实现
新增的发布订阅模式
js
// 4. promise有三个状态
const STATUS = {
PADDING: "PADDING", // padding
FULFILLED: "FULFILLED", // fulfilled
REJECTED: "REJECTED", // rejected
};
class Promise {
// 2. 传入一个执行器函数
constructor(executor) {
// 4. 默认情况下,Promise的状态就是padding;
this.status = STATUS.PADDING;
this.value = undefined; // 5. 成功的原因,默认undefined,为了方便在then里面取到,所以实例上放一份
this.reason = undefined; // 5. 失败的原因,默认undefined,为了方便在then里面取到,所以实例上放一份
this.onFulfilledCallBack = []; // 存放成功回调的
this.onRejectedCallBack = []; // 存放失败回调的
// 成功回调(注意使用箭头函数,否则theis会有问题)
const resolve = (val) => {
// 4. 只有padding时候才能改变状态
if (this.status === STATUS.PADDING) {
// 4. 调用resolve时候,将状态置位成功fulfilled
this.status = STATUS.FULFILLED;
// 5. 调用resoove时候,接收到成功的原因val
this.value = val;
// 发布:调用成功时候,将所有成功的方法执行
this.onFulfilledCallBack.forEach((fn) => fn());
}
};
// 失败回调(注意使用箭头函数,否则theis会有问题)
const reject = (reason) => {
// 4. 只有padding时候才能改变状态
if (this.status === STATUS.PADDING) {
// 4. 调用reject时,将promise置位失败,rejected
this.status = STATUS.REJECTED;
// 5. 调用reject时候, 接收到失败原因reason;
this.reason = reason;
// 发布: 执行失败时候,将所有失败的函数执行
this.onRejectedCallBack.forEach((fn) => fn());
}
};
// 6.可以在执行器函数内抛出异常,这里可以用try catch来捕获异常
try {
// 2. 执行器函数默认就会执行,
// 3.执行器函数接收个参数,分别是resolve,reject,都是函数
executor(resolve, reject);
} catch (e) {
// 6. 捕获到异常会导致promise失败
reject(e);
}
}
// 7. then是promise的实例方法,有两个参数,onFulfilled,onRejected
then(onFulfilled, onRejected) {
// 8. 若是当前promise的状态是成功则调用第一个参数onFulfilled
if (this.status === STATUS.FULFILLED) {
onFulfilled(this.value); // 8. 将成功的原因传递出去
}
// 9.若是当前promise的状态是失败则调用第二个参数onRejected
if (this.status === STATUS.REJECTED) {
onRejected(this.reason); // 9.将失败的原因传递出去
}
// 订阅模式:状态为padding时候,将方法缓存起来,等待发布..
if (this.status === STATUS.PADDING) {
this.onFulfilledCallBack.push(() => {
// todo..其他逻辑
onFulfilled(this.value);
});
this.onRejectedCallBack.push(() => {
// todo.. 其他逻辑
onRejected(this.reason);
});
}
}
}
module.exports = Promise;
本节githubCommit记录:exector加入可执行异步的逻辑,用了发布订阅模式和装饰模式
5. 测试
结果:两个then过两秒后同时输出结果
js
const Promise = require("./myPromise");
let p = new Promise((resolve, reject) => {
setTimeout(() => { // exector里面执行异步代码
resolve("ok");
}, 1000);
});
p.then(
(res) => {
console.log("success:" + res); // 过2秒后输出 success: ok
},
(err) => {
console.log("fail:" + err);
}
);
p.then(
(res) => {
console.log("resolve: " + res); // 过2秒后输出 resolve: ok
},
(err) => {
console.log("shibai: " + err);
}
);
三. 改进:链式调用(难点)
1. 要点
- 若then方法中返回的不是一个promise,无论成功(参数1)或者失败(参数2),都会将这个值传或错误递给下一次then的第一个参数成功结果内。
- 若是then方法中抛出异常,会将错误信息传递给下一次then的第二个参数的失败里去。
- 若then里面又返回了一个promise,则会将这个promise的结果作为下一次then的成功或者失败
- catch可以理解为就是then的别名,只有then的第二个参数
.then(null,(err)=>{}
- 错误处理会先找then的第二个参数,若没有则往下一个继续找,找到了就不找了,不管后面还有没有then或catch
小结:只有当then里面抛出异常或返回的promise出错,才会走到下一次then的失败里去,其他都会走下一次then的成功。
2. then为什么可以链式调用?
因为每次调用then都会返回一个全新的promise,才可能实现链式调用,因为promise状态改变了就不可能再改变了。
3. 实现
3.1 第一个then返回的不是一个promise时的链式调用
then里面可以返回不是promise的数据,也可以抛出异常,对于抛出的异常,可以在下一个then的第二个参数里面取到,对于返回的是基本值或对象,则可以在下一个then的第一个参数内取到,若没有返回任何值(没有return),则下一个then里面取到的为undefined
3.1.1 链式第一步
由于promise的状态只能改变一次,且只有padding时候才能改变,因此,链式调用的关键就是每次调用then方法都得返回一个全新的promise,用Promise2表示
js
then(onFulfilled, onRejected) {
// then返回一个promise才能链式调用,所以,每次调用then的时候都需要有一个全新的promise
let promise2 = new Promise((resolve, reject) => {
...原来的逻辑都放在这里
});
return promise2;
}
3.1.2 链式第二步
我们在使用promise
时候,then
方法的回调里面可以return
出来一个值,那么在promise
源码里面可以用一个x
来接收,当我们在then
的第一个参数里面return
出来这个值,若promise是成功状态,走了then的第一个参数,那么在下一个then的第一个参数就可以得到上一个then里面return出来的值,我们在源码里面可以通过判断promise的状态,来决定将x放到这个promise2的resolve还是reject里面
js
// promise原型上的then方法
then(onFulfilled, onRejected) {
// then返回一个promise才能链式调用,所以,每次调用then的时候都需要有一个全新的promise
let promise2 = new Promise((resolve, reject) => {
if (this.status === STATUS.FULFILLED) {
// 这里的x就是then里面return 出来的结果
let x = onFulfilled(this.value); // 8. 将成功的原因传递出去
resolve(x);
}
...其他状态同理
});
return promise2;
}
3.1.3 链式第三步
我们在调用then方法时候,,可以抛出异常,且抛出的异常会在下一个then调用时候会走到第二个参数。由于执行then方法的回调,在源码里面得到的是x
,因此,可以在源码的x这里加上try catch
,用来捕获异常,捕获到异常后,将异常交给promise2
的`reject``,依便传给下一个then的第二个参数。
js
// promise原型上的then方法
then(onFulfilled, onRejected) {
// then返回一个promise才能链式调用,所以,每次调用then的时候都需要有一个全新的promise
let promise2 = new Promise((resolve, reject) => {
if (this.status === STATUS.FULFILLED) {
try {
// 这里的x就是then里面return 出来的结果
let x = onFulfilled(this.value); // 8. 将成功的原因传递出去
resolve(x);
} catch (error) {
reject(error)
}
}
...其他状态同理
});
return promise2;
}
本节的githubCommit记录:then里面return出非promise数据
3.2 第一个then返回的是一个promise时的链式调用
在使用原生promise时候,then方法里面可以return出来一个promise,比如下面的场景: 一个axios返回了数据,下一个接口需要用这个接口返回的数据再发起一次请求,那么就会在上一个接口的then里面去发请求,然后return出去,在下下个then里面去拿到数据(只是为了举例而举例)
js
this.$request
.get("/getArticleInfo?id=1")
.then(({ commentId }) => {
return this.$request.get(`/getComment?commentId=${commentId}`);
})
.then(
(res) => {
console.log("评论信息" + res);
},
(err) => {
console.log("err" + err);
}
);
这种情况下第一个then返回的就是promise,那么x
就是一个promise,在3.1
的源码里因为x
就是一个值,因此直接将x给了promise2
的resolve
和reject
,但是这里的x
可能是一个promise
,所以这一步的关键就是将x
这个promise
的值得到,然后再传给promise2
的resolve
和reject
,问题就得以解决。
这里的判断和逻辑可以参考promise/A+规范里面的判断,只需要参照规范来实现具体逻辑即可,再者,我们的promise 按照规范来写 就可以和别人的promise共用
3.2.1 链式第四步
需要有一个方法,根据promise/A+规范,方法内部要判断x
是否为promise
,若x为promise,先将x的值获取到,再传给promise2的resolve和reject,若不是,则直接将x往下传,还需要判断x和promise2的关系,若x就是promise2,则抛出类型错误,因为不可能自己等待自己完成xxx
js
then(onFulfilled, onRejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === STATUS.FULFILLED) {
try {
let x = onFulfilled(this.value);
// resolve(x);
resolvePromise(x, promise2, resolve, reject); // 用来解析x是promise的情况
} catch (error) {
reject(error);
}
}
...其他状态同理
});
return promise2;
}
// 解析promise的方法
function resolvePromise(x, promise2, resolve, reject) {
console.log(x, promise2, resolve, reject);
}
3.2.2 链式第五步
3.2.1
中存在一个问题,就是在promise2
内部去调用 resolvePromise(x, promise2, resolve, reject)
时候,明显拿不到promise2
这个实参,因为在调用这个方法的时候,promise还没有初始化完成,因此,这里需要用一个宏任务或者微任务,等待promise2实例创建好后,再来取promise2,在promise/A+规范里面也有说明(跳转到Notes)
js
then(onFulfilled, onRejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === STATUS.FULFILLED) {
// setTimeout这里可以使用其他宏任务或微任务,目的是让里面的resolvePromise方法能拿到promise2
setTimeout(()=>{
try {
let x = onFulfilled(this.value);
// resolve(x);
resolvePromise(x, promise2, resolve, reject); // 用来解析x是promise的情况
} catch (error) {
reject(error);
}
},0)
}
...其他状态同理
});
return promise2;
}
这一节之前的githubCommit记录:链式调用之then返回promise
3.2.3 链式第六步(核心)
现在就该编写resolvePromise
方法的逻辑了,这是整个promise的核心,也是难点 前置条件:
x
:是使用promise
时候,then
里面回调的返回值,可能是普通js对象(引用和原始),也可能是一个promise实例对象。promise2
是源码里面then
方法返回的一个promise
实例对象resolve
是promise2
执行器里面的resolve
回调reject
是promise2
执行器里面的reject
回调
要做的
- 若
x
是promise
,则递归解析,得到里面的具体值; - 若
x
是普通js对象(包括原始值),则把值给resolv,因为resolvePromise
是在fulfilled
状态下调用的。 - 若
x
和promise2
是同一个promise
,则抛出异常,防止自己等于自己 - 还需要用一个状态锁保证状态不被重复修改,当状态改变后,就不能再掉resolve和reject来改变状态了
实现如下
js
function resolvePromise(x, promise2, resolve, reject) {
if (x === promise2) {
// 防止then里面return了和上一个then里面相同引用的promise,导致递归引用
throw TypeError("不允许递归引用同一个promise对象");
}
// x可能是一个promise
if ((typeof x === "object" && x !== null) || typeof x === "function") {
let called; // 调用锁,防止多次改变状态
try {
let then = x.then;
// 是promise
if (typeof then === "function") {
// 需要拿到x的状态,根据状态调用resolve和reject
// 因为上面已经拿了一次x里面的then,为了避免出错,这里直接使用,不再次拿取,但是为了保证then里面的this指向,用call调用then
// 调用返回的promise 用他的结果 作为下一次then的结果
then.call( x, function (y) {
if (called) return;
called = true;
// 递归解析成功后的值 直到他是一个promise为止
resolvePromise(y, promise2, resolve, reject);
},
function (err) {
// then的第二个参数
if (called) return;
called = true;
reject(err);
});
} else {
if (called) return;
called = true;
// x是普通对象,这里为什么不用判断状态,是因为resolvePromise就是fulfilled下调用的,因此直接resolve即可
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
// 取then时候出错,进入到下一个then的第二个参数
reject(error);
}
} else {
// x就是一个普通值
reject(x);
}
}
3.3 给then方法添加默认的回调
原生promise里面,then方法允许不传入回调,若没有传入回调,则也会将上一个then里面的内容传递给下一个then
js
let p2 = new Promise((resolve, reject) => {
resolve("okk");
});
p2.then()
.then()
.then()
.then(
(data) => {
console.log("data:" + data); // data:okk
},
(err) => {
console.log("err:" + err);
});
实现原理,其实只需要给源码里面的then方法加上默认的回调函数即可
js
// 默认处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : data=>data
onRejected = typeof onRejected === 'function' ? onRejected : err=>{ throw err } // 这里抛出异常或调用reject才会进入下一个then的第二个参数
本节githubCommit记录:给then方法添加默认回调
3.4 自动化测试promise是否符合promise/A+规范
myPromise.js里面编写测试代码
js
// 测试时会调用此方法
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
};
然后全局安装自动化测试插件
js
npm install promises-aplus-tests -g
运行命令测试
js
promises-aplus-tests myPromise.js
之后会自动跑刚刚写好的peomise,若全绿就说明写的peomise是符合规范的,如下图:
四. 其他原理
1. promisify
原理
nodejs里面的异步api都是错误优先的api,即第一个参数都是err对象,这在开发中写起来错误处理和嵌套都不利于阅读,但是node也提供promise形式的异步API,这里基于promise封装了一个类似 node里面promisify的方法
js
function promisify(fn){
return function (...args) {
return new Promise((resolve,reject)=>{
fn(...args,function (err,data) {
if(err) reject();
resolve(data);
})
});
}
}
let read = promisify(fs.readFile);