[JavaScript] 自顶向下学习如何手写promise
引子
去年写了一篇有关promise的手写文章,写到一半发现自己的理解还不是很透彻,写的很烂,今年卷土重来,实现部分采用功能分解,目录跳转的形式呈现,力求最通俗易懂得剖析promise,我们开始吧。
通过本文你能学到什么:
- 为什么需要用到promise
- 链式调用的概念及如何实现
- 发布订阅模式在promise中的应用
- 递归思想
- 一步步带你分解功能,手写实现一个promise,包括静态方法:all、allSettled、race、resolve、reject
- 如何测试自己写的promise是否符合规范
1. 认识promise
promise是ES6原生实现的一种解决异步编程的方案,我们都知道js是一门单线程语言,使用异步编程(事件和回调函数)的方式进行定时器任务、网络请求等。不了解promise用法的同学建议看看这一篇《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版 - promise对象》。
✅举个例子:
看一个简单的异步任务定时器的例子:
// 模拟异步任务
function fn(s, callback) {
setTimeout(() => {
console.log(s);
callback(s);
}, 2000);
}
// 第一种: 使用回调函数
fn("1", (s) => {
s += "2";
fn(s, () => {
s += "3";
fn(s, () => {
s += "4";
fn(s, () => {
s += "5";
fn(s, () => {
console.log("result:", s);
console.log('done')
});
});
});
});
});
// 第二种: 使用promise
function promiseFactory(str) {
return new Promise((resolve, reject) => {
fn(str, resolve);
});
}
promiseFactory("1")
.then((res) => {
res += "2";
return promiseFactory(res);
})
.then((res) => {
res += "3";
return promiseFactory(res);
})
.then((res) => {
res += "4";
return promiseFactory(res);
})
.then((res) => {
res += "5";
return promiseFactory(res);
})
.then((res) => {
console.log("result:", res);
console.log('done')
});
上面代码中两种方法都输出:
1
12
123
1234
12345
result: 12345
done
在第一种写法中可以看到回调层层嵌套,阅读起来十分困难,这还只是个简单的例子,实际中的代码可能涉及多种异步任务相互嵌套,形成回调地狱。
在第二种写法中使用了promise,采用链式调用的方式代替嵌套,虽然代码量看起来更多了一些,但在学习了promise的用法之后,阅读起来轻松了许多,更符合人从上到下线性阅读的习惯。而这正是promise诞生的原因。
🌸 Promise本质及特性
promise本质是一个构造函数,也可以说是一个类,因此可以通过new实例化出promise对象,它有以下这些特性:
- 有三种状态:pending、fullfilled、rejected
- 允许发生的状态变化(除以下两种之外都不允许):
- pending -> fullfilled(通过resolve触发状态改变)
- pending -> rejected(通过reject触发状态改变)
- 支持链式调用
- then方法是异步微任务
- 错误冒泡
最核心的就是一个promise对象通过resolve或reject方法触发状态的改变,状态改变后就固定下来了,不可再发生变化,当状态改变后会在then方法中接收参数并继续下一步操作
比较有意思的是resolve和reject方法的妙用,这两个方法也可以作为参数进行传递,实现控制反转,这个我们先按下不表,之后再深入了解。
2. 实现promise
哪里不会点哪里~
2.1 基本框架
📑 用例:
const p1 = new Promise((resolve, reject) => {
resolve("1");
}).then(
(result) => {
console.log(result);
}
);
const p2 = new Promise((resolve, reject) => {
reject("1");
}).then(
(reason) => {
console.log(reason);
}
);
🌸 实现步骤分解:
- 是个类
- 有三个状态
status = 'pending' | 'fullfilled' | 'rejected'
- 有成功状态的值
value
和失败状态的值reason
- 实例化时接收一个函数
executor
作为参数,该函数有两个参数resolve
和reject
用于控制状态变化和传递结果 - 有一个
then
方法,接收两个回调函数(成功回调和失败回调)作为参数,这两个回调在对应状态获取对应的值进行操作
💡 说白了,promise有三个成员变量status,value,reason,我们需要关注这三者的关系,并想办法去约束和控制它们
📝代码实现:
const PENDING = "pending";
const FULLFILLED = "fullfilled";
const REJECTED = "rejected";
class MyPromise {
// 接收executor
// 注意executor函数的内部执行逻辑是由用户自定义的,但他的参数resolve和reject却是在promise内部定义的
// 相当于promise内部定义好了resolve,reject,并执行executor将这两个函数返回给用户去调用
// 这里有点绕,需要反复斟酌
constructor(executor) {
this.status = PENDING;
this.value = null;
this.reason = null;
// 改变状态并赋值给value
const resolve = (value) => {
if (this.status === PENDING) {
this.value = value;
this.status = FULLFILLED;
}
};
// 改变状态并赋值给reason
const reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
}
};
// 执行executor
// 执行过程中可能报错,交由reject处理
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
// then方法在对应状态执行对应的回调函数
// 注意使用的技巧和上面的executor异曲同工
// 由用户定义好回调的执行逻辑,再在promise中执行回调,将状态对应的值暴露给用户进行操作
then(onFullfilled, onRejected) {
if (this.status === FULLFILLED) {
onFullfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
}
}
// 测试时将用例中的Promise改成MyPromise
2.2 支持异步
📑 用例:
promise是支持等待异步任务的,在异步任务中调用resolve之后,then中的成功回调才会执行,因此我们在pending
状态时需要用一个数组将回调存起来,当resolve触发时, 再执行回调。reject的情况可以类比resolve
并且promise支持一个promise对象调用多次then(注意不是链式调用),因此也是需要使用到上文中讲到的数组,不过还得配合其他方法实现(#2.4),先按下不表。
// 异步支持用例:
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1')
}, 1000)
}).then(
(result) => {
console.log(result);
}
);
🌸 实现步骤分解:
- 定义一个成员变量
callbacks
(回调数组) - 对
pending
状态进行处理:将回调加入回调数组
📝代码实现:
const PENDING = "pending";
const FULLFILLED = "fullfilled";
const REJECTED = "rejected";
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = null;
this.reason = null;
this.callbacks = [];
const resolve = (value) => {
if (this.status === PENDING) {
this.value = value;
this.status = FULLFILLED;
// resolve时调用回调数组里的所有成功回调
this.callbacks.forEach((callback) => {
callback.fullfilledCB();
});
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
// resolve时调用回调数组里的所有失败回调
this.callbacks.forEach((callback) => {
callback.rejectedCB();
});
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFullfilled, onRejected) {
if (this.status === FULLFILLED) {
onFullfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
// 当pending时,加入回调数组
if (this.status === PENDING) {
this.callbacks.push({
fullfilledCB: () => {
onFullfilled(this.value);
},
rejectedCB: () => {
onRejected(this.reason);
},
});
}
}
}
// 测试时将用例中的Promise改成MyPromise
2.3 then本身是异步的微任务
promise是微任务,js引擎会在同步任务执行完成后,再执行微任务,因此“同步任务”先输出。在我们自己的实现中,可以用setTimeout
来模拟异步任务。
需要注意的我们只是模拟实现,setTimeout
是宏任务,与微任务还是有一些区别,在此不赘述。
📑 用例:
const p3 = new Promise((resolve, reject) => {
resolve("1");
}).then((result) => {
console.log("then调用:", result);
return result + "2";
});
console.log("同步任务");
💶 运行结果:
同步任务
then调用: 1
🌸 实现步骤分解:
- then方法是一个异步任务,实际上是说回调函数的执行是异步的,因此在每个执行回调函数的地方,使用
setTimeout
包裹 - 有三处地方使用到了了回调函数,也就是状态改变时的三个地方
📝代码实现:
const PENDING = "pending";
const FULLFILLED = "fullfilled";
const REJECTED = "rejected";
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = null;
this.reason = null;
this.callbacks = [];
const resolve = (value) => {
if (this.status === PENDING) {
this.value = value;
this.status = FULLFILLED;
this.callbacks.forEach((callback) => {
callback.fullfilledCB();
});
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
this.callbacks.forEach((callback) => {
callback.rejectedCB();
});
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFullfilled, onRejected) {
if (this.status === FULLFILLED) {
// 异步化
setTimeout(() => {
onFullfilled(this.value);
});
}
if (this.status === REJECTED) {
// 异步化
setTimeout(() => {
onRejected(this.reason);
});
}
if (this.status === PENDING) {
this.callbacks.push({
fullfilledCB: () => {
// 异步化
setTimeout(() => {
onFullfilled(this.value);
});
},
rejectedCB: () => {
// 异步化
setTimeout(() => {
onRejected(this.reason);
});
},
});
}
}
}
// 测试时将用例中的Promise改成MyPromise
2.4 then的链式调用与值穿透
promise支持链式调用,原理就是then方法返回了一个新的promise对象。
单纯的返回一个新的promise是不够的,我们还需要考虑如何让当前promise的值能传到新的promise中。正如在前面说的,promise的本质其实就是操控内部的值和状态,因此我们需要定义一个方法resolvePromise,将新的promise的resolve、reject(用于控制状态)、回调返回的值(也就是需要透传的值)、以及新的promise本身,这四项作为参数交由resolvePromise进行处理
注意:这里是promise的重点
📑 用例:
const p4 = new Promise((resolve, reject) => {
resolve("1");
})
.then((result) => {
console.log("then第一次调用:", result);
return result + "2";
})
.then((result) => {
console.log("then第二次调用:", result);
return result + "3";
})
// then就算什么都不传,也会进行值的传递
.then()
.then((result) => {
console.log("then第四次调用:", result);
throw "在成功回调中抛出异常";
})
.then(
(result) => {
// 不执行
console.log(result);
},
(reason) => {
// 执行了,输出: 结果触发了失败回调
console.log(reason, "结果触发了失败回调");
return "失败回调执行后,继续返回";
}
)
.then(
(result) => {
console.log(result, "结果触发成功回调");
throw "又一次在成功回调中抛出异常";
},
(reason) => {
console.log(reason, "结果触发下一个失败回调");
}
)
.then(
() => {},
(reason) => {
console.log(reason, "此时会触发失败回调");
throw "接着在失败回调中抛出异常";
}
)
.catch((reason) => {
console.log(reason, "触发了catch");
return "catch之后继续返回";
})
.then((result) => {
console.log(result, "触发了成功回调");
return new Promise((resolve, reject) => {
resolve("返回一个新的promise, 并执行resolve");
});
})
.then((result) => {
console.log(result, "结果触发成功回调,resolve中的值继续传递");
return new Promise((resolve, reject) => {});
})
// 以下不会触发
.then((result) => {
result(
"返回一个新的promise, 不执行resolve",
"触发成功回调, 并获得了传递过来的promise对象, 调用resolve后"
);
})
.then((result) => {
console.log(result, "结果触发下一个成功回调");
});
💶 运行结果:
then第一次调用: 1
then第二次调用: 12
then第四次调用: 123
在成功回调中抛出异常 结果触发了失败回调
失败回调执行后,继续返回 结果触发成功回调
又一次在成功回调中抛出异常 此时会触发失败回调
接着在失败回调中抛出异常 触发了catch
catch之后继续返回 触发了成功回调
返回一个新的promise, 并执行resolve 触发成功回调,resolve中的值继续传递
💡 我将执行的结果放到代码中输出,对各个值传递的情况进行了举例,建议将代码复制到IDE中执行一下
📄 结论:
- promise的值作为resolve的参数层层下传
- 若then没有参数,或者参数不为两个函数,会将之前的promise值继续传递
- 在then中,只要回调函数抛出异常,就会触发下一个失败回调函数,若没有失败回调,会触发catch
- 在then中,只要回调函数直接返回值,就会触发下一个成功回调函数
- 若then中的回调函数返回的是promise对象,并执行resolve,会将结果直接传递给下一个then的成功回调
- 若then中的回调函数返回的是promise对象,并不执行resolve或reject,并不会将这个promise返回给下一个成功回调,这是因为返回值为promise对象的话promise会直接调用这个promise的then方法,但是由于并没有执行resolve或reject,所以什么都不会触发
💡 上述用例并不全面,很多rejected(失败)的情况可以类比fullfilled(成功)的清况。
通过上面的例子,会发现对于then的返回值我们最主要要进行的处理就是:
- 判断它是一个普通值还是一个promise对象,并对于它不同的取值进行不同的处理(直接返回值或是调用then方法)
此外还需要解决:
- 排除返回promise自身的情况(避免循环引用,promise自己等待自己)
- promise多层嵌套情况(使用递归)
- 返回值是promise时,可能存在的promise不规范导致的成功回调和失败回调同时调用或多次调用的情况(暂时看不懂没关系,后面会解释)
🌸 实现步骤分解:
-
判断循环引用,如果promise传递的值与其自身是相同的,手动抛出一个错误
-
思考如何判断类型:
- 首先promise肯定是一个对象或函数,不是对象直接resolve值
- 接下来判断是否有then方法,没有的话,认为他不是promise,直接resolve
- 上述条件都满足,就认为他是一个promise(这里并不好准确判断,因为除了原生的的promise,用户是可以自己实现promise的,因此该promise可能并不规范,只是满足了上述两个判断罢了,所以就会出现问题3)
-
解决问题3,定义一个called变量用于标记回调是否已经执行,防止重复执行
-
是一个promise的话,就以这个promise为作用域(使用call显式绑定this),执行then方法
-
执行then方法,其实又是要我们去处理promise的值,只不过这个promise的值是新产生的需要传递的,那么这个值可能是普通值,也可能是一个promise。
有没有感觉又回到了原点,当产生这个感觉的时候,用递归就对了。
我们的resolvePromise方法本身就是处理这种情况的,只需要改变一下参数递归调用即可。
-
在then方法中调用resolvePromise,调用的时机就是在状态改变时,执行回调并传入resolvePromise
-
注意异步任务中的错误无法被外部的trycatch捕获,因此要在异步任务中添加trycatch
-
最后解决then无参数的情况:方法是将直接将两个回调赋值为函数。
需要注意的是前面说到的结论3和4,因此成功回调可以直接返回值,而失败的回调必须抛出错误
📝完整代码实现:
const PENDING = "pending";
const FULLFILLED = "fullfilled";
const REJECTED = "rejected";
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.callbacks = [];
const resolve = (value) => {
if (this.status === PENDING) {
this.value = value;
this.status = FULLFILLED;
this.callbacks.forEach((callback) =>
callback.fullfilledCB(value)
);
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
this.callbacks.forEach((callback) =>
callback.rejectedCB(reason)
);
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFullfilled, onRejected) {
// 参数统一化,统一将两个参数设为函数
if (typeof onFullfilled !== "function")
onFullfilled = (value) => {
return value;
};
if (typeof onRejected !== "function")
onRejected = (reason) => {
// 注意这里要抛出,否则会触发成功回调
throw reason;
};
let newPromise = new MyPromise((resolve, reject) => {
if (this.status === FULLFILLED) {
setTimeout(() => {
// 异步任务需要另外trycatch
try {
// 使用resolvePromise处理值
this.resolvePromise(
newPromise,
onFullfilled(this.value),
resolve,
reject
);
} catch (error) {
reject(error);
}
});
}
if (this.status === REJECTED) {
setTimeout(() => {
// 异步任务需要另外trycatch
try {
// 使用resolvePromise处理值
this.resolvePromise(
newPromise,
onRejected(this.reason),
resolve,
reject
);
} catch (error) {
reject(error);
}
});
}
if (this.status === PENDING) {
this.callbacks.push({
fullfilledCB: () => {
setTimeout(() => {
// 异步任务需要另外trycatch
try {
// 使用resolvePromise处理值
this.resolvePromise(
newPromise,
onFullfilled(this.value),
resolve,
reject
);
} catch (error) {
reject(error);
}
});
},
rejectedCB: () => {
setTimeout(() => {
// 异步任务需要另外trycatch
try {
// 使用resolvePromise处理值
this.resolvePromise(
newPromise,
onRejected(this.reason),
resolve,
reject
);
} catch (error) {
reject(error);
}
});
},
});
}
});
return newPromise;
}
// 处理值传递
resolvePromise(newPromise, result, resolve, reject) {
// 防止循环引用
if (result === newPromise)
reject(new TypeError("Chaining cycle detected for promise"));
// 判断是否为一个对象或函数
if (
result !== null &&
(typeof result === "object" || typeof result === "function")
) {
// called用于标记回调是否被调用过(规定回调只能调用一次)
// 成功回调调用了,失败回调就不能调用,反之亦然
// 会出现调用多次的情况是因为我们不能控制传递进来的值是一个符合标准规范的promise,当然我们本文写的promise并不会发生
let called = false;
// 取then的时候可能会报错,原因还是由于result的不确定性
// 可能出现被object.defineProperty或proxy代理了,取then时抛出错误
try {
let then = result.then;
if (typeof then === "function") {
// 显示绑定this为result(传递的值)
// 递归执行回调
// 当promise嵌套promise时,我们应该等待内部的promise处理好,再返回给外部
then.call(
result,
(value) => {
// 如果执行过了这个成功回调,直接跳过
if (called) return;
// 标记为已执行
called = true;
// 递归调用
this.resolvePromise(
newPromise,
value,
resolve,
reject
);
},
(reason) => {
// 如果执行过了这个成功回调,直接跳过
if (called) return;
// 标记为已执行
called = true;
// 直接通过reject抛出错误
reject(reason);
}
);
} else {
// then不是函数的情况
if (called) return;
called = true;
resolve(result);
}
} catch (error) {
// 取then抛出错误的情况
if (called) return;
called = true;
reject(error);
}
} else {
// 是普通类型的值的情况
resolve(result);
}
}
// 这两个就很简单啦,其实就是then的语法糖
// 看不懂的话建议再理解一下前面的
catch(onRejected) {
return this.then(null, onRejected);
}
finally(onFinally) {
return this.then(onFinally, onFinally);
}
}
// 测试:
// npm install promises-aplus-tests --save-dev
// 在终端输入:promises-aplus-tests 路径
// 例如: promises-aplus-tests ./myPromise.js
MyPromise.defer = MyPromise.deferred = function () {
let dfd = {};
dfd.promise = new MyPromise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
};
module.exports = MyPromise;
3. 测试
MyPromise.defer = MyPromise.deferred = function () {
let dfd = {};
dfd.promise = new MyPromise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
};
module.exports = MyPromise;
# 终端
npm install promises-aplus-tests --save-dev
npx promises-aplus-tests [路径](例如: ./myPromise.js)
# 测试结果
872 passing
4. 静态方法
MyPromise.resolve = function (value) {
if (value && typeof value === "object" && value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve) => {
resolve(value);
});
};
MyPromise.reject = function (value) {
return new MyPromise((_, reject) => {
reject(value);
});
};
// 简易版,未处理map、set这类可迭代类型
MyPromise.all = function (promises) {
return new MyPromise((resolve, reject) => {
let count = 0;
let result = [];
if (promises.length === 0) return resolve([]);
promises.forEach((promise, index) => {
MyPromise.resolve(promise)
.then((res) => {
result[i] = res;
if (count === promises.length) resolve(result);
})
.catch(reject);
});
});
};
MyPromise.race = function (promises) {
return new MyPromise((resolve, reject) => {
promises.forEach((promise) => {
MyPromise.resolve(promise).then(resolve).catch(reject);
});
});
};
MyPromise.allSettled = function (promise) {
return new MyPromise((resolve, reject) => {
let count = 0;
const len = promises.length;
const result = [];
if (len === 0) return resolve([]);
promises.forEach((promise, index) => {
MyPromise.resolve(promise)
.then((res) => {
count += 1;
result[index] = {
status: "fullfilled",
value: res,
};
if (count === len) resolve(result);
})
.catch((err) => {
count += 1;
result[index] = {
status: "rejected",
reason: err,
};
if (count === len) resolve(result);
});
});
});
};
参考:
- 面试官:“你能手写一个 Promise 吗”
- [致全网那些所谓的手写Promise.all](致全网那些所谓的手写Promise.all - 知乎 (zhihu.com))
- 珠峰-手写Promise A+ 完整版
- 阮一峰-promise
- Promises/A+
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通