异步处理的概念
JavaScript 中的异步处理指的是在代码执行过程中,能够不阻塞当前线程并处理一些时间较长的操作。异步处理通常涉及到回调函数、Promise、async/await 等机制。
在 JavaScript 中,传统的同步处理方式采用的是阻塞式的单线程模型。这种模型的缺点是当一个任务被执行时,它会一直执行到结束,期间如果有耗时的操作也会一直阻塞下去,直到任务执行完毕,才会执行后续的任务。这种方式会导致页面卡死,体验非常不好。
因此,JavaScript 异步处理机制应运而生,它允许在代码执行过程中,执行一些耗时的操作,而不会阻塞当前线程。异步处理机制可以通过以下方式实现:
回调函数
概念: 函数被当参数传递进去. 在 JS 函数作为参数的专门名称就是callback.
回调函数有两种类型:
同步回调函数
常见的有forEach(遍历数组)、filter(过滤数组,筛选数组)、reduce(数组求和)
arr.forEach((value, index) => {
console.log(value, index);
})
let f = arr.filter(value) => {
return value % 2 != 0
})
let r =arr.reduce((total, value) => {
return total += value
}, 0)//total初始值为0
异步回调函数
常见的有addEventListener事件监听、setInterval定时器、setTimeout延时器
回调函数是一种很常见的异步编程模型,通过在异步操作完成后调用回调函数来通知异步操作已结束,从而执行后续的任务。
<div id="wrapper"></div>
<button id="btn">获取数据</button>
<script>
const btnDom=document.querySelector("#btn");
const erapperDom=document.quereSelector("#wrapper");
const fetchData= (callback) =>{
//模拟请求,服务会在1s后返回数据
setTimeout(() => {
//这块逻辑其实不应该耦合在这里
//因为这块是操作视图部分(view)的内容,不应该与服务层(service)的内容耦合在一起
//wrapperDom.innerHTML="data";
//一个函数中间的数据怎么给外面: 1.return 2.回调
//不能直接这样: return data;
//我们需要给它加上callback
callback("hello world");//传入实参 这里只负责把请求的数据给外面,并不关心外面怎么处理该数据
},1000);
};
btnDom.addEventListener(
"click",
() => {
wrapperDom.innerHTML = "loadding...";
//fetchData();
fetchData(function (data){
wrapperDom.innerHTML =data;
//do something
})//形参接收
},
false
);//点击btn按钮后会先出现loadding...然后fetchData,在1s后显示得到数据
</script>
- > 耦合: 各种不相关的功能都放在了一起,下次再想用某个单个功能,就很麻烦.
简化整理一下就是这样:
function fetchData(callback) {
setTimeout(function() {
const data = { name: '张三', age: 20 };
callback(data);//传入实参
}, 1000);
}
fetchData(function(data) {
console.log(data);
});//接收形参
在这个示例中,fetchData()函数在完成数据加载后,调用 回调函数 callback() 并传递数据作为参数。当数据加载完成后,控制器会跳转到回调函数中执行后续任务。
Promise
Promise 是一种比较流行的异步编程模型,它可以在异步操作完成后执行一些回调操作,并将结果返回给请求方。Promise 代表了一个异步操作的最终完成(或失败)及其结果值。 例如:
function fetchData() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const data = { name: '张三', age: 20 };
resolve(data);
}, 1000);
});
}
fetchData().then(function(data) {
console.log(data);
});
在这个示例中,fetchData() 函数返回了一个 Promise 对象,当数据加载完成后,Promise 对象会调用then()方法中的回调函数,并将数据作为参数传递进去。
async/await
async/await 基于 Promise 编写,并提供了更加精简的语法。async/await 语法可以使异步代码看起来像同步代码,从而写出更加直观和易于维护的代码。
async function fetchData() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const data = { name: '张三', age: 20 };
resolve(data);
}, 1000);
});
}
async function main() {
const data = await fetchData();
console.log(data);
}
main();
在这个示例中,fetchData() 函数依然返回一个 Promise 对象,但是在主函数 main() 中,我们使用了 async 关键字来定义一个异步函数,并使用 await 关键字等待fetchData()函数执行结果。当 fetchData() 函数执行完成后,数据会作为 await 表达式的结果返回,在控制台输出数据信息。
异步处理常见场景与处理策略
异步处理常见场景包括但不限于:
- 网络请求:当请求数据需要一定的时间才能返回时,为了避免用户体验受到影响,需要进行异步处理。
- 定时任务:定时执行任务,需要进行异步处理。
- 事件处理:通过异步处理来避免事件处理函数执行时间过长,导致页面卡顿等问题。
- 大量数据处理(web worker):对于大量数据的处理,需要进行异步处理,以免阻塞主线程。
异步处理的常见处理策略有:
- 回调函数:将后续处理逻辑以函数的形式传入异步操作的回调函数中,异步操作完成后自动执行回调函数。
- Promise:通过返回 Promise 对象,在异步操作完成后进行下一步处理。
- Async/await:在异步函数中使用 await 等待异步操作完成后再进行下一步处理。
- 事件监听:通过监听异步操作所触发的事件,在事件处理函数中进行后续处理。例如通过
XMLHttpRequest对象的onreadystatechange事件进行处理。 - 发布订阅模式:在异步操作完成后,发布事件,订阅事件的相关回调函数处理后续逻辑。
- Web Worker:将耗时的计算任务交给 Web Worker 在后台处理,避免主线程阻塞。
- Generator 函数:通过 Generator 函数中的
yield关键词(中断操作),将多步骤的异步操作转化为同步代码的执行模式,提高代码可读性。 - Promise.all:如果存在多个异步操作,需要等待所有操作完成后再进行处理,可以使用
Promise.all将多个 Promise 对象合并处理。 - 回调地狱解决方案:当存在多层嵌套的回调函数时,回调函数的代码难以阅读和维护。通过使用 Promise 和 async/await 进行代码重构,提高可读性和可维护性。
- 预加载资源:对于一些大型的资源,通过异步进行资源预加载可以提高页面速度和用户体验。
- 异步流程控制库:各种异步流程控制库(如 async)可以帮助简化异步过程中的问题,提高代码可读性和维护性。
回调函数与“回调地狱”
CPS 与 DS
传递风格(Continuation-passing style, CPS,与之对比是直接风格(Direct style)。
直接风格的范例如下,其实就是一般函数调用的方式:
//直接風格 DS
function func(x) {
return x
}
CPS 风格就不是这样,它会用 另一个函数作为函数中的传入参数,然后将本来应该要回传的值(不限定只有一个),传给下一个延续函数,继续下个函数的执行:
//CPS風格
function func(x, cb) {
cb(x)
}
通常,我们使用回调函数是迫不得已,因为我们没办法立即拿到值,我们就只能寄希望于事件循环机制
JavaScript 引擎是单线程执行的,也就是说同一时间内只有一个任务在执行。当需要进行异步操作时,通常会使用回调函数。
假设我们有一个获取用户信息的异步函数 getUserInfo,在信息获取完成后需要调用相关回调函数。一种实现方式是将回调函数作为 getUserInfo 函数的第二个参数传入,信息获取完成后调用该函数。
function getUserInfo(userId, callback) {
setTimeout(function() {
const userInfo = {
id: userId,
name: "Tom",
age: 25
}
callback(userInfo)
}, 1000)
}
getUserInfo(1001, function(userInfo) {
console.log(userInfo)
})
上述代码首先调用 getUserInfo 函数,该函数通过 setTimeout 模拟异步操作,等待 1 秒钟后获取用户信息,并在信息获取完成后调用传入的回调函数。最后在回调函数中输出用户信息。
我们再来看一个更可怕的(回调地狱),这个案例我们贯穿始终
function f1(f2){
f2(function(f3){
f3(function(f4) {
f4(function () {});
});
});
}
f1(function(f2) {
const t1 = 'f1';
console.log(t1);
f2(function(f3) {
const t2 = `${t1},f2`;
console.log(t2);
f3(function(f4) {
const t3 = `${t2},f3`;
console.log(f3);
f4();
});
});
});
从 callback 到 promise
回调函数最让人诟病的就是嵌套了,为此开发者们还为它取了个名字——“回调地狱”。
同样是以上 getUserInfo 示例,如果我们通过 promise 来处理,是怎样的呢?
function getUserInfo(userId) {
return new Promise((resolve, reject) => {
setTimeout(function() {
const userInfo = {
id: userId,
name: "Tom",
age: 25
}
resolve(userInfo)
}, 1000)
})
}
getUserInfo(1001).then(res => console.log(res))
一般用 Promise 方案进行处理时,我们需要关注以下几个点:
- 函数一定返回 promise 实例(return new Promise((resolve,reject) =>{});)
- promise 实例中需要进行 resolve、reject 处理
- 使用时,通过 then 进行链式调用并获取结果
- 如果有异常,可以在 then 第二个参数回调或者 .catch 进行处理
- 还包括 finally
如果 callback 嵌套上述贯穿始终的回调地狱示例,我们用 promise 改写,会怎样?
f1()
.then(() => {
const t1 = 'f1';
console.log(t1);
return t1;
})
.then(t1 => {
return f2().then(function() {
const t2 = `${t1},f2`;
console.log(t2);
return t2;
});
})
.then(t2 => {
return f3().then(function() {
const t3 = `${t2},f3`;
console.log(f3);
return t3;
});
});
这样看,其实还是繁琐,代码不够简洁,没事儿,我们会在后面继续将它优化。
就算 Promise 相较于 callback 代码简化了很多,但是我们通常还是抱怨 Promise 定义太繁琐,有没有更简洁的方式呢?
详解 Promise A+ 规范
- 解决(fulfill):指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但在后世的 promise 实现多以 resolve 来指代
- 拒绝(reject):指一个 promise 失败时进行的一系列操作
Promise 的状态
一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending) 、执行态(Fulfilled) 和 拒绝态(Rejected) 。
- 等待态(Pending) 处于等待态时,promise 需满足以下条件:
-
- 可以迁移至执行态或拒绝态
- 执行态(Fulfilled) 处于执行态时,promise 需满足以下条件:
-
- 不能迁移至其他任何状态
-
- 必须拥有一个不可变的终值
- 拒绝态(Rejected) 处于拒绝态时,promise 需满足以下条件:
-
- 不能迁移至其他任何状态
-
- 必须拥有一个不可变的原因
-
- 这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(注:当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。
then 方法
一个 promise 必须提供一个 then 方法以访问其当前值、终值(promise 被解决时传递给解决回调的值) 和 据因(promise 被拒绝时传递给拒绝回调的值)。
promise 的 then 方法接受两个参数:
promise.then(onFulfilled, onRejected);
其中,onFulfilled 和 onRejected 都是可选参数。
- 如果 onFulfilled 不是函数,其必须被忽略
- 如果 onRejected 不是函数,其必须被忽略
手写 Promise
手写 Promise 前,首先我们加深一个概念,那就是发布-订阅模式,这个概念非常重要。
我们可以用类来实现Promise,而且Promise有三种状态:pending、fulfilled、rejected。初始状态为pending。还需要对Promise的终值进行初始化。Promise还有两个方法resolve和reject。
Promise有四个特点:
- 执行了resolve,Promise状态就会变成fulfilled
- 执行了reject,Promise状态就会变成rejected
- Promise状态不可逆,第一次成功就永久为fulfilled,第一次失败就永久为rejected
- Promise中有throw的话,就相当于执行了rejected
const STATUS = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected",
};
// 首先有一个 myPromise 的类
class myPromise {
status = STATUS.PENDING; // 默认状态是 pending
// 用于 then 中的回调函数存储,以便在 resolve 时执行
resolves = [];
rejects = [];
// subscribers = [];
// webpack 中 tiptap 的源码中的写法
resolve = (value) => {
// 1. 改变状态
// 2. 保存值
// 3. 触发回调
const { resolves } = this;
// 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
if (this.status !== STATUS.PENDING) return;
this.status = STATUS.FULFILLED;
// 把我们现在存储在 resolves 中的 resolve 依次执行 出队
while (resolves.length) {
const resolveFn = resolves.shift();
const newVal = resolveFn(value);
}
};
reject = (value) => {
const { rejects } = this;
// 把我们现在存储在 rejects 中的 reject 依次执行 出队
// 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
if (this.status !== STATUS.PENDING) return;
this.status = STATUS.REJECTED;
while (rejects.length) {
const rejectFn = rejects.shift();
const newVal = rejectFn(value);
}
};
constructor(executor) {
executor(this.resolve, this.reject);
}
then(resolveFn, rejectFn) {
// .then 就意味着我需要订阅你的状态变更后的值 入队
this.resolves.push(resolveFn);
rejectFn && this.rejects.push(rejectFn);
console.log("then");
return this;
}
catch(rejectFn) {
this.rejects.push(rejectFn);
console.log("catch");
}
finally() {
console.log("finally");
}
}
// 然后有一个 myPromise 的实例
const myPromise = new myPromise((resolve, reject) => {
setTimeout(() => {
resolve("hello world");
}, 1000);
});
myPromise
.then((res) => {
// 我是非常期望在一秒以后,能够打印 hello world
console.log(res);
return "res" + res;
})
.then((res) => {
console.log(res + "111");
});
// .catch((err) => {
// console.log(err);
// });
console.log(myPromise);
基础实现
我们需要记录需要进行 resolve 的操作,然后在promise 执行 then 时,可以调起该 resolve 并进行处理。
JS:
class myPromise{
resolves = [];
constructor(executor){
executor(this.resolve);
}
resolve = (value) => {
const{ resolves } = this;
while(resolves.length){
const resolveFn = resolves.shift();//出队
const newVal = resolveFn(value);
}
}
then(resolveFn){
this.resolves.push(resolveFn);//入队
}
}
const myPromise = new myPromise((resolve, reject) => {
setTimeout(() => {
resolve("hello world");
}, 1000);
});
TS:
type FuncType = (...args: any[]) => any;
type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;
export class myPromise {
private resolves: FuncType[] = [];
constructor(executor: ExecutorFunc) {
const { resolve } = this;
executor(resolve);
}
private resolve = (resolvedVal: any) => {
const { resolves } = this;
while (resolves.length) {
const cb = resolves.shift();
if (cb) cb(resolvedVal);
}
};
then(resolveFunc: FuncType) {
this.resolves.push(resolveFunc);
}
}
完整执行过程是:当执行 new myPromise() 时,constructor 函数会执行,不过这里需要注意的是,我们暂时只考虑异步操作,忽略了同步的情况。异步情况下 executor 函数会在未来某个时间点执行,而从初始化到这个时间点之间,正是 then 函数执行收集依赖的过程。
添加 reject 的处理
JS:
class myPromise{
resolves = [];
rejects = [];
constructor(executor){
executor(this.resolve,this.reject);
}
resolve = (value) => {
const{ resolves } = this;
while(resolves.length){
const resolveFn = resolves.shift();//出队
const newVal = resolveFn(value);
}
}
reject = (value) => {
const { rejects } = this;
while(rejects.length){
const rejectFn = rejects.shift();
constnewVal = rejectFn(value);
}
}
then(resolveFn){
this.resolves.push(resolveFn);//入队
rejectFn && this.rejects.push(rejectFn);
}
}
TS:
type FuncType = (...args: any[]) => any;
type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;
export class HePromise {
private resolves: FuncType[] = [];
private rejects: FuncType[] = [];
constructor(executor: ExecutorFunc) {
const { resolve, reject } = this;
executor(resolve, reject);
}
private resolve = (resolvedVal: any) => {
const { resolves } = this;
while (resolves.length) {
const cb = resolves.shift();
if (cb) cb(resolvedVal);
}
};
private reject = (rejectedVal: any) => {
const { rejects } = this;
while (rejects.length) {
const cb = rejects.shift();
if (cb) cb(rejectedVal);
}
};
then(resolveFunc: FuncType, rejectFunc?: FuncType) {
this.resolves.push(resolveFunc);
if (rejectFunc) this.rejects.push(rejectFunc);
}
}
使用 jest 进行测试
首先配置 jest 环境
npm install --save-dev jest
在 package.json 中修改执行脚本:
{
"scripts": {
"test": "jest"
}
}
编写对应测试用例
describe('test HePromise', () => {
it('basic usage', done => {
const p = new HePromise(resolve => {
setTimeout(() => {
resolve(1);
}, 1000);
});
try {
p.then(data => {
expect(data).toBe(1);
done();
});
} catch (error) {
done(error);
}
});
});
最后执行 pnpm test:
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
执行测试,测试通过,完成 Promise 初始版本封装。执行流程总结如下:
- Promise 构造方法需要传入一个函数,我们将这个函数命名为
executor; - 在
executor内部,将各任务放入宏/微任务队列中(宏/微任务请参看 事件循环 ); - 在
then和catch中可收集到resolve、reject依赖,并将该依赖存放到对应队列中; - 异步任务执行完以后,调用
executor中的resolve或reject,取出对应队列中的依赖依次执行。
增加符合 Promise A+ 规范的状态值
我们为 myPromise 添加状态,根据规范约定,在代码中添加状态枚举值,如下:
//TS
enum STATUS {
PENDING = 'pending',
FULFILLED = 'fulfilled',
REJECTED = 'rejected',
}
//JS
const STATUS = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected",
};
在执行 resolve 前,需要检测当前状态是否为 pending,如果是则可以继续执行,否则无法执行 resolve,在执行 resolve 时,将状态置为 fulfilled。reject 方法中同理先检测状态是否为 pending,如果是则继续执行并将状态置为 rejected。
改进后,代码示例如下:
JS:
const STATUS = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected",
};
class myPromise {
status = STATUS.PENDING; // 默认状态是 pending
resolves = [];// 用于 then 中的回调函数存储,以便在 resolve 时执行
rejects = [];
constructor(executor) {
executor(this.resolve, this.reject);
}
resolve = (value) => {
const { resolves } = this;
// 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
if (this.status !== STATUS.PENDING) return;
this.status = STATUS.FULFILLED;
while (resolves.length) {//出队
const resolveFn = resolves.shift();
const newVal = resolveFn(value);
}
};
reject = (value) => {
const { rejects } = this;
// 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
if (this.status !== STATUS.PENDING) return;
this.status = STATUS.REJECTED;
while (rejects.length) {
const rejectFn = rejects.shift();
const newVal = rejectFn(value);
}
};
then(resolveFn, rejectFn) {
this.resolves.push(resolveFn);//入队
rejectFn && this.rejects.push(rejectFn);
}
}
TS:
type FuncType = (...args: any[]) => any;
type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;
enum STATUS {
PENDING = 'pending',
FULFILLED = 'fulfilled',
REJECTED = 'rejected',
}
export class HePromise {
private status = STATUS.PENDING;
private resolves: FuncType[] = [];
private rejects: FuncType[] = [];
constructor(executor: ExecutorFunc) {
const { resolve, reject } = this;
executor(resolve, reject);
}
private resolve = (resolvedVal: any) => {
const { resolves, status } = this;
if (status !== STATUS.PENDING) return;
this.status = STATUS.FULFILLED;
while (resolves.length) {
const cb = resolves.shift();
if (cb) cb(resolvedVal);
}
};
private reject = (rejectedVal: any) => {
const { rejects, status } = this;
if (status !== STATUS.PENDING) return;
this.status = STATUS.REJECTED;
while (rejects.length) {
const cb = rejects.shift();
if (cb) cb(rejectedVal);
}
};
then(resolveFunc: FuncType, rejectFunc?: FuncType) {
this.resolves.push(resolveFunc);
if (rejectFunc) this.rejects.push(rejectFunc);
}
}
支持链式调用
根据 Promise A+ 规范,每次 then 返回的值也需要满足 thenable,也就是说我们需要将 resolve 返回值使用 promise 包裹,在本例中就是需要将返回值包装为新的 myPromise 对象。 开发之前我们不妨先来看看Promise 链式调用的示例:
const p = new Promise(resolve => resolve(1));
p.then(r1 => {
console.log(r1);
return 2;
})
.then(r2 => {
console.log(r2);
return 3;
})
.then(r3 => {
console.log(r3);
});
每次 then 函数调用完,都返回了一个新的数字,令人不解的是,这个数据居然也拥有了 then 函数,可以依次调用。这里需要做的处理是,需要将传入的 resolve 与 reject 函数封装然后放入待执行队列中。简言之,当返回值为一个 Promise 时,需要执行 promise.then 方法,否则直接执行 resolve。 改进后的 then 方法如下:
JS:
then(resolveFn, rejectFn) {
return new myPromise((resolve, reject) => {
const resolvedFn = (val) => {
try {
let resolvedVal = resolveFn(val);
//如果返回的是一个 Promise,那么就需要等待这个 Promise 执行完毕
resolvedVal instanceof myPromise
? resolvedVal.then(resolve, reject)
: resolve(resolvedVal);
} catch (error) {
if (reject) reject(error);
}
};
this.resolves.push(resolvedFn);
if (rejectFn) this.rejects.push(rejectFn);
});
}
TS:
then(resolveFunc: FuncType, rejectFunc?: FuncType) {
return new myPromise((resolve, reject) => {
const resolvedFn = (val: any) => {
try {
let resolvedVal = resolveFunc(val);
resolvedVal instanceof myPromise
? resolvedVal.then(resolve, reject)
: resolve(resolvedVal);
} catch (error) {
if (reject) reject(error);
}
};
this.resolves.push(resolvedFn);
if (rejectFunc) this.rejects.push(rejectFunc);
})
}
可以看到,then 方法调用时,会返回新的 myPromise 对象,该对象中主要做了这样几件事情:
- 包装初始 then 方法传入的 resolve 函数;
- 先将初始 then 方法传入的 resolve 函数执行,得到返回值,如果返回值是一个新的 myPromise 对象,则需要手动调用该实例的 then 方法,否则直接执行 resolve 函数;
- 将包装过的 resolve 函数放入 resolves 队列中,等待执行
补全 reject 的逻辑
JS:
then(resolveFn, rejectFn) {
return new myPromise((resolve, reject) => {
const resolvedFn = (val) => {
try {
let resolvedVal = resolveFn(val);
//如果返回的是一个 Promise,那么就需要等待这个 Promise 执行完毕
resolvedVal instanceof myPromise
? resolvedVal.then(resolve, reject)
: resolve(resolvedVal);
} catch (error) {
if (reject) reject(error);
}
};
const rejectFn = (val) => {
try {
let rejectedVal = rejectFn(val);
rejectedVal instanceof myPromise
? rejectedVal.then(resolve, reject)
: resolve(rejectedVal);
} catch (error) {
if (reject) reject(error);
}
};
this.resolves.push(resolvedFn);
if (rejectFn) this.rejects.push(rejectFn);
});
}
TS:
then(resolveFunc: FuncType, rejectFunc?: FuncType) {
return new myPromise((resolve, reject) => {
const resolvedFn = (val: any) => {
try {
const resolvedVal = resolveFunc(val);
resolvedVal instanceof HePromise
? resolvedVal.then(resolve, reject)
: resolve(resolvedVal);
} catch (error) {
if (reject) reject(error);
}
};
this.resolves.push(resolvedFn);
const rejectedFn = (val: any) => {
if (rejectFunc) {
try {
const rejectedVal = rejectFunc(val);
rejectedVal instanceof HePromise
? rejectedVal.then(resolve, reject)
: resolve(rejectedVal);
} catch (error) {
if (reject) reject(error);
}
}
};
if (rejectFunc) this.rejects.push(rejectedFn);
});
}
编写更多测试用例,进行测试
it('chain invoke usage', done => {
const p = new myPromise(resolve => {
setTimeout(() => {
resolve(11);
}, 1000);
});
try {
p.then(data => {
expect(data).toBe(11);
return 'hello';
})
.then(data => {
expect(data).toBe('hello');
return 'world';
})
.then(data => {
expect(data).toBe('world');
done();
});
} catch (error) {
done(error);
}
});
执行测试,可以看到测试用例通过。
不过需要注意的是,根据 Promise A+ 规范,需要对 then 参数进行处理,如果参数不是函数,则需要忽略并继续往下执行,示例如下:
typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;
typeof rejectFunc !== 'function'
? (rejectFunc = reason => {
throw new Error(reason instanceof Error ? reason.message : reason);
})
: null;
值过滤与状态变更
与此同时,如果在执行过程中,Promise 状态值已发生变化,则需要根据不同状态直接进行相应,例如,如果是 pending,则将任务放入对应队列中,如果为 fulfilled,直接调用 resolve,如果为 rejected 则直接调用 reject。 可以使用 switch 语句进行策略处理,如下:
switch (this.status) {
case STATUS.PENDING:
this.resolves.push(resolvedFn);
this.rejects.push(rejectedFn);
break;
case STATUS.FULFILLED:
resolvedFn(this.value);
break;
case STATUS.REJECTED:
rejectedFn(this.value);
break;
}
此处 this.value 是上次执行完后得到的值,起到暂存的目的。补充以上代码后,完整代码示例如下:
type FuncType = (...args: any[]) => any;
type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;
enum STATUS {
PENDING = 'pending',
FULFILLED = 'fulfilled',
REJECTED = 'rejected',
}
export class myPromise {
private status = STATUS.PENDING;
private value = undefined;
private resolves: FuncType[] = [];
private rejects: FuncType[] = [];
constructor(executor: ExecutorFunc) {
const { resolve, reject } = this;
executor(resolve, reject);
}
private resolve = (resolvedVal: any) => {
const { resolves, status } = this;
if (status !== STATUS.PENDING) return;
this.status = STATUS.FULFILLED;
this.value = resolvedVal;
while (resolves.length) {
const cb = resolves.shift();
if (cb) cb(resolvedVal);
}
};
private reject = (rejectedVal: any) => {
const { rejects, status } = this;
if (status !== STATUS.PENDING) return;
this.status = STATUS.REJECTED;
this.value = rejectedVal;
while (rejects.length) {
const cb = rejects.shift();
if (cb) cb(rejectedVal);
}
};
then(resolveFunc: FuncType, rejectFunc?: FuncType): myPromise {
typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;
typeof rejectFunc !== 'function'
? (rejectFunc = reason => {
throw new Error(reason instanceof Error ? reason.message : reason);
})
: null;
return new myPromise((resolve, reject) => {
const resolvedFn = (val: any) => {
try {
const resolvedVal = resolveFunc(val);
resolvedVal instanceof HePromise
? resolvedVal.then(resolve, reject)
: resolve(resolvedVal);
} catch (error) {
if (reject) reject(error);
}
};
this.resolves.push(resolvedFn);
const rejectedFn = (val: any) => {
if (rejectFunc) {
try {
const rejectedVal = rejectFunc(val);
rejectedVal instanceof myPromise
? rejectedVal.then(resolve, reject)
: resolve(rejectedVal);
} catch (error) {
if (reject) reject(error);
}
}
};
switch (this.status) {
case STATUS.PENDING:
this.resolves.push(resolvedFn);
this.rejects.push(rejectedFn);
break;
case STATUS.FULFILLED:
resolvedFn(this.value);
break;
case STATUS.REJECTED:
rejectedFn(this.value);
break;
}
});
}
}
同步任务处理
以上情况我们遗漏了一个点,就是同步任务,我们可以看到以上示例中,初始化 myPromise 中的 resolve 都是在未来进行的,如果同步执行 resolve,则以上代码会出现问题。我们的方案是,将初始处理默认放入宏任务队列中,也就是使用 setTimeout 包裹 resolve,这样一来,就能保证即使是同步任务,也可以保证在同步收集完任务以后在执行 executor 中的 resolve 和 reject。示例如下:
export class myPromise {
private resolve = (resolvedVal: any) => {
setTimeout(() => {
const { resolves, status } = this;
if (status !== STATUS.PENDING) return;
this.status = STATUS.FULFILLED;
this.value = resolvedVal;
while (resolves.length) {
const cb = resolves.shift();
if (cb) cb(resolvedVal);
}
});
};
}
同理可实现 reject 逻辑。
编写测试代码,如下:
it('sync task', done => {
const p = new myPromise(resolve => {
resolve(123);
});
p.then(res => {
expect(res).toBe(123);
done();
});
});
其他方法实现
Promise 中还包括 catch、finally、Promise.resolve、Promise.reject、Promise.all、Promise.race,接下来我们分别来实现。
catch
其实我们可以理解是 then 方法的一个变体,就是 then 方法省略了 resolve 参数,实现如下:
catch(rejectFnnc) {
return this.then(undefined, rejectFnnc)
}
finally
该方法保证 Promise 不管是 fulfilled 还是 reject 都会执行,都会执行指定的回调函数。在 finally 之后,还可以继续 then。并且会将值原封不动的传递给后面的 then 函数。
针对这个机制也有很多理解,糙版的处理如下:
finally(cb) {
return this.then(
value => {
cb();
return value;
},
reason => {
cb();
throw reason
}
)
}
不过,如果 Promise 在 finally 前返回了一个 reject 状态的 promise,像上面这样编写是无法满足要求的。
finally 对自身返回的 promise 的决议影响有限,它可以将上一个 resolve 改为 reject,也可以将上一个 reject 改为另一个 reject,但不能把上一个 reject 改为 resolve。
这样一来,我们可以将 callback 使用 Promise.resolve 包裹一下,保证后续的 resolve 状态。如下:
finally(cb) {
return this.then(
value => myPromise.resolve(cb()).then(() => value),
reason => myPromise.resolve(cb()).then(() => { throw reason })
)
}
resolve
调用该静态方法其实就是将值 promise 化,如果传入值本身就是 promise 示例,则直接返回,否则创建新的 promise 示例并返回,示例如下:
static resolve(val) {
if(val instanceof HePromise) return val
return new myPromise(resolve => resolve(val))
}
编写测试代码如下:
it('myPromise.resolve', done => {
myPromise.resolve(1).then(res => {
expect(res).toBe(1);
done();
});
});
reject
该方法的原理同 resolve,直接贴出代码
static reject(val) {
return new myPromise((resolve, reject) => reject(val))
}
编写测试代码如下:
it('myPromise.reject & catch', done => {
myPromise.reject(1).then(
res => {
expect(res).toBe(1);
done();
},
error => {
expect(error).toBe(1);
done();
},
);
});
或者通过 catch 的方式,如下:
it('myPromise.reject & catch', done => {
myPromise.reject(1)
.then(res => {
expect(res).toBe(1);
done();
})
.catch(error => {
expect(error.message).toEqual('1');
done();
});
});
执行测试,测试通过。
all
就是将传入数组中的值 promise 化,然后保证每个任务都处理后,最终 resolve。 示例如下:
myPromise.all = function (promises) {
let index = 0;
const result = [];
const pLen = promises.length;
return new myPromise((resolve, reject) => {
promises.forEach((p) => {
myPromise.resolve(p).then(
(val) => {
index++;
result.push(val);
if (index === pLen) {
resolve(result);
}
},
(err) => {
if (reject) reject(err);
}
);
});
});
};
编写测试用例如下:
it('myPromise.all', done => {
myPromise.all([1, 2, 3]).then(res => {
expect(res).toEqual([1, 2, 3]);
done();
});
});
执行测试,测试通过。
race
就是将传入数组中的值 promise 化,只要其中一个任务完成,即可 resolve。示例如下:
myPromise.race = function (promises) {
return new myPromise((resolve, reject) => {
promises.forEach((p) => {
myPromise.resolve(p).then(
(val) => {
resolve(val);
},
(err) => {
if (reject) reject(err);
}
);
});
});
};
编写测试用例:
it('myPromise.race', done => {
myPromise.race([11, 22, 33]).then(res => {
expect(res).toBe(11);
done();
});
});
执行测试,测试通过。
async 与 await 用法及原理详解
基本使用
async function test() {
const res = await Promise.resolve(1)
return res
}
需要注意的是,使用 async、await 处理异步操作时,需要注意异常的处理。
异常处理
通常我们使用 try、catch 捕获 async、await 执行过程中抛出的异常,就像这样:
async function test() {
let res = null
try {
const res = await Promise.resolve(1)
return res
} catch(e) {
console.log(e)
}
}
从零实现一个类似 async、await 的函数
function fn(nums) {
return new Promise(resolve = >{
setTimeout(() = >{
resolve(nums * 2)
},
1000)
})
}
function * gen() {
const num1 = yield fn(1)
const num2 = yield fn(num1)
const num3 = yield fn(num2)
return num3
}
function generatorToAsync(generatorFn) {
return function() {
return new Promise((resolve, reject) = >{
const g = generatorFn() const next1 = g.next() next1.value.then(res1 = >{
const next2 = g.next(res1) // 传入上次的res1
next2.value.then(res2 = >{
const next3 = g.next(res2) // 传入上次的res2
next3.value.then(res3 = >{
// 传入上次的res3
resolve(g.next(res3).value)
})
})
})
})
}
}
const asyncFn = generatorToAsync(gen)
asyncFn().then(res = >console.log(res)) // 3秒后输出 8
自动执行
自动执行其实就是运用递归,将生成器函数产生的数据不断调用 next,直至执行完成。
function getData(endpoint) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Data received from ${endpoint}`)
}, 2000)
})
}
// 生成器函数
function* getDataAsync() {
const result1 = yield getData('Endpoint 1')
console.log(result1)
const result2 = yield getData('Endpoint 2')
console.log(result2)
return 'All data received'
}
// 将生成器函数包装成 Promise
function asyncToPromise(generatorFn) {
const generator = generatorFn()
function handleResult(result) {
if (result.done) {
return Promise.resolve(result.value)
}
return Promise.resolve(result.value)
.then(res => handleResult(generator.next(res)))
.catch(err => handleResult(generator.throw(err)))
}
try {
return handleResult(generator.next())
} catch (error) {
return Promise.reject(error)
}
}
asyncToPromise(getDataAsync).then(result => console.log(result))