「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
最近准备跳槽,一直在学习和刷面试题。这两天重新学习一下Promise。
虽然一年前手撕过Promise,但基本上都是参考很多文章,然后很多复制粘贴,很多地方没有深入学习。现在回头学习了一边,然后纯手写实现,这个过程收获不少新东西。当然,实现后,我整理出这篇笔记,同样也能收获到一些新知识哈哈哈。
什么是Promise
首先先带大家过一遍
Promise,这不是基础教学,如果没用Promise的朋友可以先去学一学。
ECMAScript6提供了Promise对象,而它最主要的作用,就是用来监听一个异步操作的完成或失败。
在没有Promise之前,我们想要监听一个异步操作的结束然后执行某些操作的时候,一般都是通过回调函数来实现,最典型的例子就是setTimeout。
setTimeout(() => {
doSomething();
}, 1000);
上面的例子,实现了一个定时器,在1秒后触发执行回到函数,进而执行doSomething()函数。
但如果,我们想在1秒后,再设置一个定时器的话:
setTimeout(() => {
setTimeout(() => {
doSomething();
}, 1000);
}, 1000);
甚至说再来几个的话:
setTimeout(() => {
setTimeout(() => {
setTimeout(() => {
setTimeout(() => {
doSomething();
}, 1000);
}, 1000);
}, 1000);
}, 1000);
这就是很典型的回调地狱了。而回调地狱,最明显的缺点,就是嵌套太多,大大影响了代码的可读性和逻辑。
而这种案例在现实项目中其实并不少见,有时候我们一个页面需要请求多个接口,而一些接口的请求数据需要依赖上一个接口的响应数据,这时候就不得不得等待上一个接口的响应。
而Promise的诞生,很好的解决了回调地狱这个问题。
首先我们可以创建一个Promise示例,然后传入一个执行函数executor。这个executor函数接收两个参数,分别为resolve函数和reject函数。
new Promise((resolve, reject) => {
// doSomething
});
先来说说resolve函数。它实质上是用来改变Promise的状态,就是告诉Promise说异步函数执行成功了。因此,我们在异步操作执行结束的时候,需要调用一下resolve函数。
同时,resolve函数可以接收一个参数value,它是作为异步操作成功的返回值,传递给下一步操作。
相反的,reject函数就是代表异步操作执行失败了,同时它也可以接收一个参数reason,作为异步操作失败的原因。
new Promise((resolve, reject) => {
try {
setTimeout(() => {
// 执行成功
resolve('success');
}, 1000);
} catch (e) {
// 执行失败
reject('fail');
}
})
紧接着,我们可以调用Promise的实例方法then,来执行异步完成后的操作。
then方法接收两个参数,即onFulfilled处理函数和onRejected处理函数。
-
onFulfilled函数即在异步操作执行成功后被调用,即上面执行resolve函数。并且它会接收一个value参数,即调用resolve时传入的参数。 -
onRejected函数即在异步操作执行失败后被调用,即上面执行reject函数。并且它会接收一个reason参数,即调用reject时传入的参数。
new Promise((resolve, reject) => {
try {
setTimeout(() => {
// 执行成功
resolve('success');
}, 1000);
} catch (e) {
// 执行失败
reject('fail');
}
}).then(
// onFulfilled处理函数
(value) => {
console.log(value); // 'success'
},
// onRejected处理函数
(reason) => {
console.log(reason); // 'fail'
}
);
Promse还提供了catch实例方法,可以单独处理错误情况。
new Promise((resolve, reject) => {
try {
setTimeout(() => {
// 执行成功
resolve('success');
}, 1000);
} catch (e) {
// 执行失败
reject('fail');
}
})
.then(
// onFulfilled处理函数
(value) => {
console.log(value); // 'success'
}
)
.catch(
// onRejected处理函数
(reason) => {
console.log(reason); // 'fail'
}
);
接下来,我们就用使用Promise来改造前面的回调地狱。
其实看上面的代码,我们可以发现Promise的另一个特性,就是链式调用,就是下面这个样子。
new Promise().then().then().then().then()
因此,我们可以在上一个then再执行一个异步操作。但这时候问题就出现了,在then里面是没有resolve和reject函数的。
这时我们在里面新建一个Promise示例,执行异步操作。然后再将这个实例返回出去。此时then执行onFulfilled函数接收到了一个Promise实例,它会将这个实例的then操作绑定到下一个then操作中。
new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve('success 1');
}, 1000);
} catch (e) {
reject('fail');
}
})
.then((value) => {
console.log(value); // success 1
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve('success 2');
}, 1000);
} catch (e) {
reject('fail');
}
})
})
.then((value) => {
console.log(value); // success 2
});
到这里我们了解了Promise的基本使用。
关于
Promise还有很多需要学习的地方,比如实例方法finally,静态方法all、allSellled、resolve、reject、any、race。这些在后面的实现上会简单讲一下。其次就关于
Promise的执行时序,实际上就是关于JavaScript的事件循环EvenLoop,大家可以自己去学习,也可以参考一下我的文章《做一些动图,学习一下EventLoop》。其次还有关于
async/await,其实就是将Promise操作变成一个同步操作的语法糖。
最后,我们需要简单学习一下Promise的状态。Promise实质上有三种状态:
-
待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
-
已兑现(fulfilled): 意味着操作成功完成。
-
已拒绝(rejected): 意味着操作失败。
每当我们初始化Promise实例后,它默认状态为pending,然后当我们调用resolve函数时,它的状态就会变成fulfilled,如果我们调用的是reject函数时,它的状态就会变成rejected。
而且,当状态处于fulfilled和rejected的时候,是不会再进行状态变化了。
因此,一个Promise实例的状态变化只会有以下三种可能:
-
pending->fulfilled,此时会调用then实例方法中的onFulfilled函数 -
pending->rejected,此时会调用then实例方法中的onRejected函数和catch实例方法中的回调函数。 -
一直处于
pending,也就是在异步操作中没有执行resolve和reject函数,这种情况也不会触发then、catch和finnlly实例方法。
这时可能大家会有一个疑问,就是前面的用Promise改造回调地狱的实现中,好像存在两次pending -> fulfilled的情况。
其实上面的代码等同于下面的代码:
new Promise((resolve, reject) => {
try {
setTimeout(() => {
// 执行成功
resolve('success 1');
}, 1000);
} catch (e) {
// 执行失败
reject('fail');
}
})
.then((value) => {
console.log(value);
new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve('success 2');
}, 1000);
} catch (e) {
reject('fail');
}
}).then((value) => {
console.log(value);
});
})
因此实质上它里面是存在两个Promise实例的,因此也会存在两个状态变化。
而之所以可以写成之前的写法,是因为Promise中会去识别onFulfilled函数和onRejected函数的返回值,如果是一个Promise实例的话,它会将它的then绑定到自身的下一个then操作上。
实现Promise
初始化
在前面我们提到过,Promise是一个对象。然后在实践上,我们会通过new关键字去构造Promise实例。因此Promise是一个构造函数,或者说,Promise是一个类。(JavaScript本身是没有类这一说的,class 只是实现构造函数的语法糖)
因此我们可以初始化一下Promise。
class Promise {
constructor() {}
}
其次我们使用一个PROMISE_STATE常量对象,来存储Promise三种状态,并且在构造函数初始化一下状态。
const PROMISE_STATE = {
PENDING: 'pending', // 待定(pending): 初始状态,既没有被兑现,也没有被拒绝
FULFILLED: 'fulfilled', // 已兑现(fulfilled): 意味着操作成功完成
REJECTED: 'rejected' // 已拒绝(rejected): 意味着操作失败
};
class Promise {
constructor(executor) {
// 初始化状态
this.promiseState = PROMISE_STATE.PENDING;
}
}
实现构造函数
通过前面的讲解,我们知道新建Promise实例时,需要传入一个执行函数executor,并且这个执行函数会接收两个参数,分别为onFulfilled函数和onRejected函数。
class Promise {
constructor(executor) {
// 初始化状态
this.promiseState = PROMISE_STATE.PENDING;
// 初始化resolve函数和reject函数
const resolve = (value) => {};
const reject = (reason) => {};
try {
// 执行 executor 函数
executor(resolve, reject);
} catch (e) {
// 如果executor执行报错,则调用reject
reject(e);
}
}
}
接下来来实现resolve和reject函数,它们的功能其实差不多。
-
首先是判断状态是否为
pending,不是的话就不继续执行了。 -
如果是
pending状态的话,则改变状态。 -
然后保存
value值或reason值。 -
最后执行
onFulfilled函数或onRejected函数。
前面几步其实都不难,主要在于最后一步。
实际上,在Promise中会有有两个实例属性onResolvedCallbacks和onRejectedCallbacks,即两个数组。
在then操作和catch操作中,它们会把所有的onFulfilled函数和onRejected函数保存到这两个实例数组中。
因此在resolve和reject方法中,只需要遍历对应数组一一执行即可。
class Promise {
constructor(executor) {
// ...
// 成功的值
this.value = undefined;
// 存储 onFulfilled 的数组
this.onResolvedCallbacks = [];
const resolve = (value) => {
// 只能在状态为pending的时候执行
if (this.promiseState === PROMISE_STATE.PENDING) {
this.promiseState = PROMISE_STATE.FULFILLED; // 修改状态
this.value = value; // 保存值
this.onResolvedCallbacks.forEach((fn) => fn()); // 调用所有 onFulfilled 回调
}
};
// 失败的原因
this.reason = undefined;
// 存储onRejected的数组
this.onRejectedCallbacks = [];
const reject = (reason) => {
if (this.promiseState === PROMISE_STATE.PENDING) {
this.promiseState = PROMISE_STATE.REJECTED; // 修改状态
this.reason = reason; // 保存失败原因
this.onRejectedCallbacks.forEach((fn) => fn()); // 调用所有 onRejectedCallbacks 回调
}
};
// ...
}
}
这时候,构造函数的实现就完成了。
实现实例方法 —— then
在then方法中,主要是实现以下几个事情:
-
判断状态
-
如果是
fulfilled,异步执行onFulfilled函数 -
如果是
rejected,异步执行onRejected函数 -
如果是
pending,将onFulfilled,onRejected分别存入onResolvedCallbacks和onRejectedCallbacks数组中
-
-
不管是执行还是存入数组,都需要封装一层进行异步执行
-
返回一个新的
promise,实现链式调用
这里比较难的是第二点——异步执行,我们可以先来看看下面的代码:
new Promise(resolve => {
console.log(1);
resolve(2);
}).then((res) => {
console.log(res);
})
console.log(3);
如果用过Promise的或者刷过Promise面试题的或者了解EvenLoop的朋友都会知道,最终的执行结果是1->3->2,尽管executor操作不是异步操作。
我们可以从EvenLoop的角度简单讲一下。
-
按照从上往下的执行顺序,首先会执行
executor函数,即首先输出1。 -
其次执行了
resolve函数,promise实例变成fulfilled状态,因此onFulfilled函数被进入微任务队列。 -
接下来跳出
Promise,执行console.log(3)。 -
此时所有同步函数执行完成了,就开始清空微任务队列,即执行
console.log(res),输出2。
而我们可以反向推一下,onFulfilled进入到微任务队列,而微任务队列其实是异步队列,因此then操作会将onFulfilled包装成异步操作。
而在我们实现上,我们会使用setTimeout来模拟异步实现。
讲完原理,我们一步步来实现。
首先我们简单处理一下传入onFulfilled函数和onRejected函数,因为这两个都是可选参数。
class Promise {
// ...
then(onFulfilled, onRejected) {
// 处理 onFulfilled 回调
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
// 处理 onRejected 回调
onRejected =
typeof onRejected === 'function'
? onRejected
: (err) => {
throw err;
};
}
}
其次,我们初始化一个Promise实例,并将其返回。
class Promise {
// ...
then(onFulfilled, onRejected) {
// ...
const newPromise = new Promise((resolve, reject) => {});
return newPromise;
}
}
接下来,我们来定义一个公用的异步处理函数asyncHandler,用于封装onFulfilled函数和onRejected函数。
class Promise {
// ...
then(onFulfilled, onRejected) {
// ...
const newPromise = new Promise((resolve, reject) => {
const asyncHandler = (fn) => {
// 使用setTimeout来模拟异步操作
setTimeout(() => {
try {
// 得到返回值
const res = fn();
// TODO 处理结果值
} catch (e) {
reject(e);
}
});
};
});
return newPromise;
}
}
紧接着,我们就可以将onFulfilled和onRejected包裹起来。这里别忘记了onFulfilled调用时需要传入value参数,onRejected调用时需要传入reason参数。
class Promise {
// ...
then(onFulfilled, onRejected) {
// ...
const newPromise = new Promise((resolve, reject) => {
// ...
// 使用异步处理函数包裹onFulfilled和onRejected
const fulfilledHandler = () => asyncHandler(() => onFulfilled(this.value));
const rejectedHandler = () => asyncHandler(() => onRejected(this.reason));
});
return newPromise;
}
}
紧接着,我们就通过判断状态分别处理fulfilledHandler和rejectedHandler。
class Promise {
// ...
then(onFulfilled, onRejected) {
// ...
const newPromise = new Promise((resolve, reject) => {
// ...
// 状态为fulfilled的时候,异步执行onFulfilled,并传入this.value
if (this.promiseState === PROMISE_STATE.FULFILLED) {
fulfilledHandler();
}
// 状态为rejected的时候,onRejected,并传入this.reason
else if (this.promiseState === PROMISE_STATE.REJECTED) {
rejectedHandler();
}
// 状态为pending的时候,将onFulfilled、onRejected存入数组
else if (this.promiseState === PROMISE_STATE.PENDING) {
this.onResolvedCallbacks.push(fulfilledHandler);
this.onRejectedCallbacks.push(rejectedHandler);
}
});
return newPromise;
}
}
这时候then的实现已经搞一段落。
但里面还有一个TODO还没完成,也就是在asyncHandler函数中,我们执行完onFulfilled或onRejected后得到返回值,需要进行处理。
因此我们可以封装一个resolvePromise函数来进行统一处理。而因为我们外面包了一层promise,因此我们处理将返回值传给resolvePromise之外,也需要将newPromise、resolve和reject也传入,便于修改这个内部promise实例的状态。
class Promise {
// ...
then(onFulfilled, onRejected) {
// ...
const newPromise = new Promise((resolve, reject) => {
// ...
const asyncHandler = (fn) => {
setTimeout(() => {
try {
const res = fn();
// 处理返回值
resolvePromise(newPromise, res, resolve, reject);
} catch (e) {
reject(e);
}
});
};
// ...
});
return newPromise;
}
}
function resolvePromise(newPromise, res, resolve, reject) {}
实现resolvePromise函数
首先,我们需要做一层边缘检测,就是避免循环引用。
function resolvePromise(newPromise, res, resolve, reject) {
// 避免循环引用使用
if (res === newPromise) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
}
其实,我们需要判断是否为对象或函数,如果不是的话直接resolve值即可。
function resolvePromise(newPromise, res, resolve, reject) {
// ...
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
// TODO
} else {
resolve(res);
}
}
紧接着,如果res是一个对象或函数的话,判断res.then()是否为一个函数,不是的话也是直接返回结果。是的话则调用then函数。
这里可能大家会有个疑问,为什么不直接用res instanceof Promise来判断res是否为Promise。这是因为,这里不仅仅只有Promise这种情况,我们可以先运行一下下面的代码:
new Promise(resolve => {
resolve({
then(resolve) {
resolve(1);
}
})
}).then(res => {
return res;
}).then(res => {
console.log(res)
})
上面的代码执行后,最终是会输出1。也就是说,但onFulfilled函数返回一个含有then函数的对象或函数,Promise也会去调用这个then函数。
我们继续完善代码:
function resolvePromise(newPromise, res, resolve, reject) {
// ...
// 如果返回值为一个对象或者函数
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
try {
const then = res.then;
// 如果返回值是一个promise或者一个带有then函数的对象
if (typeof then === 'function') {
then.call(
res,
// onFulfilled 回调
(r) => {
resolvePromise(newPromise, r, resolve, reject);
},
// onRejected 回调
(err) => {
reject(err);
}
);
} else {
resolve(res);
}
} catch (e) {
reject(e);
}
} else {
// res 为普通的值,直接返回
resolve(res);
}
}
这时候resolvePromise的功能基本实现了。
最后还需要做一层边缘检测,就是避免多次调用then函数。因此使用一个called变量来检测。
function resolvePromise(newPromise, res, resolve, reject) {
// ...
// 防止多次调用
let called = false;
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
try {
const then = res.then;
if (typeof then === 'function') {
then.call(
res,
(r) => {
// 成功和失败只能调用一个
if (called) return;
called = true;
resolvePromise(newPromise, r, resolve, reject);
},
(err) => {
// 成功和失败只能调用一个
if (called) return;
called = true;
reject(err);
}
);
} else {
resolve(res);
}
} catch (e) {
// 成功和失败只能调用一个
if (called) return;
called = true;
reject(e);
}
} else {
resolve(res);
}
}
实现实例方法 —— catch
上面已经实现了then方法,而catch实例方法接收的onRejected函数实质上跟then的onRejected一致。因此我们可以通过调用then来实现catch。
class Promise {
// ...
catch(onRejected) {
return this.then(null, onRejected);
}
}
实现静态方法 —— resolve
Promise.resolve方法会新建一个Promise实例,然后将其绑定value值并将状态设置为fulfilled,并将其返回。
实质上就是新建一个Promise实例并调用了resolve方法并将其返回。
class Promise {
// ...
static resolve(value) {
return new Promise((resolve) => resolve(value));
}
}
实现静态方法 —— reject
Promise.reject类似于Promise.resolve,它返回一个带有拒绝原因的Promise实例。
class Promise {
// ...
static reject(reason) {
return new Promise((resolve, reject) => reject(reason));
}
}
实现实例方法 —— finally
Promise.prototype.finally会在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这避免了同样的语句需要在then()和catch()中各写一次的情况。
class Promise {
// ...
finally(onFinally) {
return this.then(
(res) => Promise.resolve(onFinally()).then(() => res),
(err) =>
Promise.reject(onFinally()).then(() => {
throw err;
})
);
}
}
实现静态方法 —— all
Promise.all会接收一个存放一个或多个Promise实例的数组,然后将其封装成一个Promise实例返回,并且这个实例的resolve回调的结果也是一个数组。
我们可以通过下面的代码来了解一下:
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // [3, 42, "foo"]
});
但如果在执行过程中,数组中但凡有一个Promise实例执行错误的话,Promise.all返回的Promise实例也会直接抛出错误,不会输出已完成的结果或者继续执行未完成的Promise实例。
因此我们可以简单实现一下:
class Promise {
// ...
static all(promises) {
const results = [];
return new Promise((resolve, reject) => {
if (!promises.length) resolve(results);
// 遍历promises一一执行
for (const promise of promises) {
promise.then(
(res) => {
// 保存结果值
results.push(res);
// 当results和promises长度一致,则代表所有 promise 执行完成了
if (results.length === promises.length) {
resolve(results);
}
},
// 但凡有一个promise执行报错,直接reject回去
reject
);
}
});
}
}
实现静态方法 —— allSettled
Promise.allSettled与Promise.all类似,也是接收一个Promise实例数组,返回一个新的Promise实例。
而区别在于Promise.allSettled不管数组中的Promise实例是成功完成或者执行失败,都会对成功值或者失败原因进行报错,以及还会保存 Promise实例的状态,最后返回一个结果数组。
我们同样通过一个代码案例来看一下:
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(() => {
reject('foo')
}, 100));
const promises = [promise1, promise2];
Promise.allSettled(promises).then((results) => {
console.log(results) // [{ status: "fulfilled", value: 3 }, { status: "rejected", reason: "foo" }]
});
接下来我们来实现一下:
class Promise {
// ...
static allSettled(promises) {
const results = [];
return new Promise((resolve, reject) => {
try {
if (!promises.length) resolve(results);
// 遍历promises一一执行
for (const promise of promises) {
promise.then(
(res) => processData({ status: PROMISE_STATE.FULFILLED, value: res }),
(err) => processData({ status: PROMISE_STATE.REJECTED, reason: err })
);
}
// 处理数据
function processData(res) {
results.push(res);
if (results.length === promises.length) {
resolve(results);
}
}
} catch (e) {
reject(e);
}
});
}
}
实现静态方法 —— race
Promise.race 方法同样接收一个Promise实例数组,返回一个新的Promise实例。一旦数组中的某个Promise执行成功或执行失败,返回的Promise实例就会返回该实例的结果。
简单来说,Promise.race返回的Promise实例最终的结果,即传入的数组中最快执行结束的Promise实例的结果,不管它是成功还是失败。
同样我们通过下面的代码来学习一下:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 500);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 100);
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value); // 2 因为promise2更快完成
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => reject(3), 500);
});
const promise4 = new Promise((resolve, reject) => {
setTimeout(() => reject(4), 100);
});
Promise.race([promise3, promise4]).catch((reason) => {
console.log(reason); // 4 因为promise4更快完成
});
接下来我们就来实现一下:
class Promise {
// ...
static race(promises) {
return new Promise((resolve, reject) => {
for (const promise of promises) {
promise.then(resolve, reject);
}
});
}
}
实现静态方法 —— any
Promise.any 方法跟Promise.race类似,同样接收一个Promise实例数组,返回一个新的Promise实例。
他们的区别在于,Promise.any返回第一个成功的Promise实例,如果是执行失败的话,则会跳过执行下一个。如果所有的Promise实例都执行失败了,即抛出错误。
同样我们通过一段代码来看一下:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 500);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject(2), 100);
});
Promise.any([promise1, promise2]).then((value) => {
console.log(value); // 1 尽管promise2先执行好,但是它是执行失败,直接跳过
});
测试
如果你会jest或其他测试工具,我会建议你每实现一个功能之前,先写一个该功能的测试用例。在这里分享一下我在实现时自己写的测试用例。
从日常的学习实践上,养成单测习惯,对日后的工作和学习都是有好处的。
我现在重读红宝书也写 测试用例来进行学习的,还是比较爽的哈哈哈
而对于Promise的单元测试,那一定得是Promise/A+测试。
首先得安装一下相关插件:
npm install promises-aplus-tests -D
紧接着,在你的文件中index.js插入一下代码:
// 测试
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
};
module.exports = Promise;
然后执行下面命令进行测试:
promises-aplus-tests Promise.js
一共是872个测试用例,如果通过都会就会显示:
872 passing (17s)
而对于Promise/A+规范,跟原生的Promise还是有一定的区别,如果你想更深入的学习Promise的话,可以阅读下面这篇文章。