javascript, Promise,Es6, 事件循环(Event-Loop)
写在前面
Promise在日常开发中使用也是非常频繁,但是大多时候仅仅只是觉得它的api使用起来非常方便,它也是目前主流的异步编程解决方案;
本文主要通过手写实现Promise各种api来加深对promise的理解,阅读本文需要你有使用promise的基础以及javascript基础;同时还需要对事件循环机制有所了解,因为后续会涉及到js执行顺序的问题;
对于promise的定义之类各大博客网站介绍很多了,这里就不赘述了,咱们直接上才艺。
1.初始化
首先创建一个空目录,里边新建一个index.html文件用于编写测试例子,新建一个index.js里面写我们自定义的Promise函数,将index.js文件通过script标签引入;
index.js定义一个Promise函数(后续作为构造函数进行使用),通过定义重名函数来覆盖浏览器默认内置的Promise函数,接下来我们就往自己的Promise中添加逻辑即可;
2.实现Promise的构造器函数
为了方便理解我们先使用内置的Promise写个例子,打印输出,结果如下:
由上图我们可以看到,Promise对象实例化时需要传入一个回调函数,并且该回调函数里的代码会立即执行;同时这个函数接收两个函数参数且支持调用,成功执行resolve函数,失败执行reject函数;
根据以上例子,我们自定义的函数添加形参以及函数声明:
/**
* @description: Promise函数
* @name: 声明构造函数Promise
* @param {callback} executor 接收一个回调函数
*/
function Promise(executor) {
function resolve(value) {
console.log(value);
}
function reject(value) {
console.log(value);
}
executor(resolve, reject);
}
3.实现构造器函数中的resolve与reject
3.1基础实现
resolve,reject我们知道在调用其中一个之后,对象的状态(PromiseState)会发生改变以及对象的结果值(PromiseResult)变化;PromiseState一共三种状态分别为 pending、fulfilled(resolved这里我们用fulfilled)、rejected;
调用resolve方法状态将由pending->fulfilled, 调用reject方法状态将由pending->rejected,并且状态一旦更改为成功或失败后就不允许再改变;
显然我们也需要添加两个属性,在调用resolve与reject时对值进行修改:
/**
* @description: Promise函数
* @name: 声明构造函数Promise
* @param {callback} executor 接收一个回调函数
*/
function Promise(executor) {
this.PromiseState = 'pending';
this.PromiseResult = null;
// 报错this指向Promise实例
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
function resolve(value) {
// 状态如果已经改变过那么终止逻辑
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'fulfilled';
}
function reject(value) {
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'rejected';
}
executor(resolve, reject);
}
在调用resolve与reject函数时如果状态已经变更那么需要终止逻辑,因为Promise规定只允许变更一次状态;其次修改状态时需要注意this指向问题,因为resolve,reject作为函数单独调用this指向的是全局对象这里是window,我们希望this始终指向Promise实例对象;
这是时候再用之前的例子我们可以看输出结果已经符合预期了:
3.2 补充抛出异常逻辑
将对象状态变更为rejected除了调用reject方法外,抛出异常(throw something)也可以;但是我们上面没有写关于抛出异常的处理,因此需要补充一下;
处理异常我们都知道使用 try catch进行捕获,函数的执行是调用executor,那么我们就对它进行异常捕获;当我们捕获到异常信息,调用reject进行状态变更并将错误信息传入;
function Promise(executor) {
//....
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
输出结果:
到这里基本的resolve与reject函数实现就结束了,剩下的后续还会补充。
4.实现then方法
4.1同步任务then方法实现
我们知道Promise实例可以通过then方法指定成功或者失败的回调函数,并且在对象状态变更后能够获取的结果值;
因此我们需要给自定义的Promise的函数添加then方法,因为then方法存在于每个Promise实例身上,因此我们将then方法挂在它的原型对象(prototype)上;
then方法接收两个回调函数(这里我们分别定义为onResolve[成功],onReject[失败]),当实例的状态(PromiseState)为fulfilled调用onResolve;rejected时调用onReject函数;并且我们将对应的结果值(PromiseResult)传入onResolve,onReject函数,这样页面使用时才能接收到对应值;
function Promise() {
//这是上面写好的。。。。就不重复黏贴了
}
/**
* @description: 添加then方法
* @name: then方法的调用者是promise实例,也就是this
* @param {*} onResolve 成功回调
* @param {*} onReject 失败回调
*/
Promise.prototype.then = function (onResolve, onReject) {
if (this.PromiseState === 'fulfilled') {
onResolve(this.PromiseResult);
}
if (this.PromiseState === 'rejected') {
onReject(this.PromiseResult);
}
};
至此,结合上面写的Promsie函数,我们整个例子看看输出:
4.2 异步任务then方法实现
上面的例子中我们只考虑了同步执行的情况,当resolve或者reject在异步函数中调用的时候,那么就会出问题;
异步的情况使用setTimeout模拟,例子如下,控制台不会输出任何信息,这里就不截图了,直接分析代码;
这里需要对事件循环(Event-Loop)机制有所了解,setTimeout是典型的异步任务(宏任务),执行顺序在then方法之后;而我们代码正常运行的话(以下面例子的情况)需要先调用resolve函数,确保then方法调用之前对象状态与结果已经更改,这样才能走到if条件的逻辑里面;显然我们的then方法还缺失了对pending状态的逻辑处理,因为在调用then方法时,咱们并没有调用resolve或者reject,自然PromiseState状态也不会更改以及是默认值pending,而我们对pending的逻辑处理是缺失的;
从上面的分析,我们补充pending状态的逻辑处理;当状态为pending时,我们进行保存成功和失败的回调函数操作,但是不进行执行操作,因为我们也不知道最终状态会是什么情况;函数的执行交给实例中的resolve与reject,因为只有调用这两个函数时状态才会变化;
所以总结一下就是:
在then方法中添加pending逻辑处理,并且定义一个缓存数组用来保存回调函数;
在resolve与reject函数执行时,遍历上面的缓存数组,取出回调函数逐个执行从而触发对象状态和值的变更并且将结果值传入回调函数中,这样页面中then方法指定的回调才能拿到返回结果;
/**
* @description: Promise函数
* @name: 声明构造函数Promise
* @param {callback} executor 接收一个回调函数
*/
function Promise(executor) {
this.PromiseState = 'pending';
this.PromiseResult = null;
this.callbackList = [];
// 报错this指向Promise实例
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
function resolve(value) {
// 状态如果已经改变过那么终止逻辑
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'fulfilled';
that.callbackList?.forEach((item) => item.onResolve(value));
}
function reject(value) {
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'rejected';
that.callbackList?.forEach((item) => item.onReject(value));
}
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
/**
* @description: 添加then方法
* @name: then方法的调用者是promise实例,也就是this
* @param {*} onResolve 成功回调
* @param {*} onReject 失败回调
*/
Promise.prototype.then = function (onResolve, onReject) {
if (this.PromiseState === 'fulfilled') {
onResolve(this.PromiseResult);
}
if (this.PromiseState === 'rejected') {
onReject(this.PromiseResult);
}
if (this.PromiseState === 'pending') {
this.callbackList.push({
// 简写形式,属性名和函数同名
onResolve,
onReject,
});
}
};
写个测试例子看看结果能够发现,resolve的结果已经存在promsie对象当中了,then方法指定的成功回调也正常输出了;
4.3 同步任务then方法返回结果
参照内置的Promise方法,then方法执行后会返回一个promise实例对象,状态为成功(fulfilled),如下图:
根据上面的返回结果来看,我们的then方法调用后需要返回一个Promise实例,并把结果传入回调函数当中;同时在传入结果之前需要先判断页面中成功回调(resolve)中传入的参数是否为promise实例,如果是的话then方法的结果将由此promise实例决定,否则就调用resolve方法将结果返回;
then方法补充之后:
Promise.prototype.then = function (onResolve, onReject) {
return new Promise((resolve, reject) => {
if (this.PromiseState === 'fulfilled') {
const result = onResolve(this.PromiseResult);
if (result instanceof Promise) {
result.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(result);
}
}
if (this.PromiseState === 'rejected') {
const result = onReject(this.PromiseResult);
if (result instanceof Promise) {
result.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(result);
}
}
if (this.PromiseState === 'pending') {
this.callbackList.push({
onResolve,
onReject,
});
}
});
};
写个例子测试
4.4 异步任务then方法返回结果
修改为异步任务之后,输出的结果如下;
对比内置的Promise函数结果,显然不符合预期,内置的Promise返回结果如下;
可以发现我们的Promise函数状态并没有发生改变,而原因依旧是同步异步任务处理顺序导致的问题;当调用then方法中的回调时,setTimeout里面的函数并没有执行,所以状态没有改变依旧是pending;
目前自定义Promise函数的then方法中对于pending的处理只有保存回调的操作,但是现在我们需要将保存的回调函数进行调用从而获取结果返回;
使用try catch依旧是处理异常抛出时的失败处理;
Promise.prototype.then = function (onResolve, onReject) {
// 保证pending逻辑中函数调用时this指向正确
const that = this
return new Promise((resolve, reject) => {
if (this.PromiseState === 'fulfilled') {
const result = onResolve(this.PromiseResult);
//判断返回结果是否为promise实例,拿到返回结果
if (result instanceof Promise) {
result.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
//普通值则直接resolve返回
resolve(result);
}
}
if (this.PromiseState === 'rejected') {
const result = onReject(this.PromiseResult);
if (result instanceof Promise) {
result.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(result);
}
}
if (this.PromiseState === 'pending') {
this.callbackList.push({
onResolve: function () {
try {
const res = onResolve(that.PromiseResult);
if (res instanceof Promise) {
res.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(res);
}
} catch (err) {
reject(err);
}
},
onReject: function () {
try {
const res = onReject(that.PromiseResult);
if (res instanceof Promise) {
res.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(res);
}
} catch (err) {
reject(err);
}
},
});
}
});
};
4.5 then方法中回调异步执行
这边先给个例子,我们期望的输出顺序是 111->333->222;
内置Promise函数中调用then方法指定的回调函数是异步执行的,所以222应当最后输出,正确顺序是 111->333->222;我们希望自定义的Promise函数达到此效果,解决方式是在执行回调的地方用一个定时器包装成异步任务;
const p1 = new Promise((reslove, reject) => {
reslove('Success');
console.log(111)
});
p1.then(value => {
console.log(value)
console.log(222)
})
console.log(333)
因此我们在Promise函数中遍历执行回调方法外部使用setTimeout,then方法中在调用回调时也进行相同的处理;
4.6 then方法结束
到此为止,then方法的实现就告一段落了,接下来把重复的代码(判断返回结果是否为promise实例的处理逻辑)提取一下,最终加上之前Promise方法最终版本是这样:
/**
* @description: Promise函数
* @name: 声明构造函数Promise
* @param {callback} executor 接收一个回调函数
*/
function Promise(executor) {
this.PromiseState = 'pending';
this.PromiseResult = null;
this.callbackList = [];
// 报错this指向Promise实例
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
function resolve(value) {
// 状态如果已经改变过那么终止逻辑
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'fulfilled';
if (that.callbackList.length) {
setTimeout(() => {
that.callbackList?.forEach((item) => item.onResolve(value));
});
}
}
function reject(value) {
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'rejected';
if (that.callbackList.length) {
setTimeout(() => {
that.callbackList?.forEach((item) => item.onReject(value));
});
}
}
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
Promise.prototype.then = function (onResolve, onReject) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
return new Promise((resolve, reject) => {
//接收一个方法名,onResolve或是onReject
function callbackFn(fnName) {
try {
const res = fnName(that.PromiseResult);
if (res instanceof Promise) {
res.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(res);
}
} catch (err) {
reject(err);
}
}
if (this.PromiseState === 'fulfilled') {
setTimeout(() => {
callbackFn(onResolve);
});
}
if (this.PromiseState === 'rejected') {
setTimeout(() => {
callbackFn(onReject);
});
}
if (this.PromiseState === 'pending') {
this.callbackList.push({
onResolve: function () {
callbackFn(onResolve);
},
onReject: function () {
callbackFn(onReject);
},
});
}
});
};
5.实现catch方法
5.1基础实现
catch方法用于指定失败的回调函数,逻辑与then方法相比,它只接收一个失败的回调,因此我们直接内部调用then方法即可;
/**
* @description: 添加catch方法
* @name: catch方法的调用者是promise实例,也就是this
* @param {*} onResolve 成功回调
* @param {*} onReject 失败回调
*/
Promise.prototype.catch = function (onRejected) {
return this.then(undefined, onRejected);
};
测试
5.2 catch方法异常穿透
所谓异常穿透指的是promise实例在指定多个回调函数时,在末尾指定一个catch函数就能够捕获到之前的回调报错信息,而不是每个then方法都指定对于的catch方法,具体例子;
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('defeat');
}, 1000);
});
const res = p
.then((res) => {
console.log('111');
})
.then((res) => {
console.log(222);
})
.then((res) => {
console.log(333);
})
.catch((reason) => {
console.log(reason);
});
上面例子我们可以发现,then方法中对于pending的处理存在瑕疵;在pending状态时,我们对异步回调函数进行保存,每次都会保存一个成功与失败的回调对象(但是then方法指定的两个回调函数应当是可选的,可以不传),在不传的情况下,成功与失败的回调函数可能都不存在,因此遍历时执行函数就发生了函数未定义的情况;
解决方式就是在then方法中,判断回调函数是否存在,不存在时定义默认抛出异常的函数;
function Promise(executor) {
this.PromiseState = 'pending';
this.PromiseResult = null;
this.callbackList = [];
// 报错this指向Promise实例
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
function resolve(value) {
// 状态如果已经改变过那么终止逻辑
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'fulfilled';
if (that.callbackList.length) {
setTimeout(() => {
that.callbackList?.forEach((item) => item.onResolve(value));
});
}
}
function reject(value) {
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'rejected';
if (that.callbackList.length) {
setTimeout(() => {
that.callbackList?.forEach((item) => item.onReject(value));
});
}
}
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
Promise.prototype.then = function (onResolve, onReject) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
// 回调函数为空时设置抛出异常函数
if (typeof onReject !== 'function') {
onReject = (err) => {
throw err;
};
}
if (typeof onResolve !== 'function') {
onResolve = (val) => {
throw val;
};
}
return new Promise((resolve, reject) => {
// 执行then指定的回调函数,将结果返回
function callbackFn(fnName) {
try {
const res = fnName(that.PromiseResult);
if (res instanceof Promise) {
res.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(res);
}
} catch (err) {
reject(err);
}
}
if (this.PromiseState === 'fulfilled') {
setTimeout(() => {
callbackFn(onResolve);
});
}
if (this.PromiseState === 'rejected') {
setTimeout(() => {
callbackFn(onReject);
});
}
if (this.PromiseState === 'pending') {
this.callbackList.push({
onResolve: function () {
callbackFn(onResolve);
},
onReject: function () {
callbackFn(onReject);
},
});
}
});
};
Promise.prototype.catch = function (onRejected) {
return this.then(undefined, onRejected);
};
6. 实现Promise.resolve方法
与上面的then,catch方法不同,它是存在于Promise构造函数本身,可以直接通过Promise.xxx的方式调用,并且后续写的reject,all, race方法也是如此;
Promise.resolve方法执行后返回一个成功的promise对象,并且resolve函数接收一个参数,此参数也可以是promise对象(禁止套娃);
Promise.resolve = function (value) {
return new Promise((resolve, reject) => {
if (value instanceof Promise) {
value.then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
},
);
} else {
resolve(value);
}
});
};
测试例子
7.实现Promise.reject
与Promise.resolve不同,reject方法始终返回失败的结果;
Promise.reject = function (value) {
return new Promise((resolve, reject) => {
reject(value);
});
};
8.实现Promise.all方法
该方法接收一个promise实例数组,返回值两种情况:
所有promise实例执行成功,那么会返回一个新的promsie对象,对象值(PromiseResult)为所有成功执行的数组,状态(PromiseState)为fulfilled;
如果有执行失败的promise函数,那么返回首个状态为rejected的结果;
Promise.all = function (promiseList) {
let count = 0; //累计成功个数
let result = []; //存储结果
return new Promise((resolve, reject) => {
for (let i = 0; i < promiseList.length; i++) {
promiseList[i].then(
(res) => {
count++;
result[i] = res;
// 全部成功执行,返回结果数组
if (count === promiseList.length) {
resolve(result);
}
},
(err) => {
reject(err);
},
);
}
});
};
测试例子
9.实现Promise.race方法
race顾名思义就是看谁执行得快,该方法也是接收一个数组,与Promise.all方法不同的是,它会将第一个状态变更的promsie结果返回,不论是fulfilled还是rejected;
Promise.race = function (promiseList) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promiseList.length; i++) {
promiseList[i].then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
},
);
}
});
};
测试例子
10.使用Es6 class类写法进行改写
在上面的介绍中,生成实例对象是通过构造函数方式产生;如果有接触过Java,TypeScript这类面向对象语言的话,就会发现写法上差异很大,个人认为还是class方式的写法比较易于理解;
使用class我们需要知道,静态方法为类本身所持有,定义在类里边的方法则是所有实例对象都会拥有(默认情况下);因此改造的注意点就是then,catch方法直接定义在类中,定义时从写法上省略function关键字;resolve,all等方法前面需要使用static关键词进行修饰,以此来标识这是静态方法,挂在Promise这个类身上;
使用起来与我们上面的测试例子完全一致,这里就不贴测试代码了,可以自行测试;
/**
* @description: Promise函数
* @name: 声明构造函数Promise
* @param {callback} executor 接收一个回调函数
*/
class Promise {
constructor(executor) {
this.PromiseState = 'pending';
this.PromiseResult = null;
this.callbackList = [];
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
function resolve(value) {
// 状态如果已经改变过那么终止逻辑
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'fulfilled';
if (that.callbackList.length) {
setTimeout(() => {
that.callbackList?.forEach((item) => item.onResolve(value));
});
}
}
function reject(value) {
if (that.PromiseState !== 'pending') return;
that.PromiseResult = value;
that.PromiseState = 'rejected';
if (that.callbackList.length) {
setTimeout(() => {
that.callbackList?.forEach((item) => item.onReject(value));
});
}
}
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onResolve, onReject) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
if (typeof onReject !== 'function') {
onReject = (err) => {
throw err;
};
}
if (typeof onResolve !== 'function') {
onResolve = (val) => {
throw val;
};
}
return new Promise((resolve, reject) => {
// 执行then指定的回调函数,将结果返回
function callbackFn(fnName) {
try {
const res = fnName(that.PromiseResult);
if (res instanceof Promise) {
res.then(
(val) => {
resolve(val);
},
(err) => {
reject(err);
},
);
} else {
resolve(res);
}
} catch (err) {
reject(err);
}
}
if (this.PromiseState === 'fulfilled') {
setTimeout(() => {
callbackFn(onResolve);
});
}
if (this.PromiseState === 'rejected') {
setTimeout(() => {
callbackFn(onReject);
});
}
if (this.PromiseState === 'pending') {
this.callbackList.push({
onResolve: function () {
callbackFn(onResolve);
},
onReject: function () {
callbackFn(onReject);
},
});
}
});
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
static resolve(value) {
return new Promise((resolve, reject) => {
if (value instanceof Promise) {
value.then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
},
);
} else {
resolve(value);
}
});
}
static reject(value) {
return new Promise((resolve, reject) => {
reject(value);
});
}
static all(promiseList) {
let count = 0; //累计成功个数
let result = []; //存储结果
return new Promise((resolve, reject) => {
for (let i = 0; i < promiseList.length; i++) {
promiseList[i].then(
(res) => {
count++;
result[i] = res;
// 全部成功执行,返回结果数组
if (count === promiseList.length) {
resolve(result);
}
},
(err) => {
reject(err);
},
);
}
});
}
static race(promiseList) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promiseList.length; i++) {
promiseList[i].then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
},
);
}
});
}
}
写在最后
以上就是手写实现Promise构造函数以及部分方法,希望对大家在理解和使用Promise上能有更加深刻的理解;
文章内容总结来源于视频教程传送门