大家好,我是若川。最近组织了源码共读活动,感兴趣的可以点此加我微信ruochuan12 进群参与,每周大家一起学习200行左右的源码,共同进步。已进行4个月了,很多小伙伴表示收获颇丰。
以下问题你是不是在哪里听过?
你知道什么是 Promise 吗?它是干什么用的呢?
那你知道 Promise 有哪些方法吗?如何使用呢?
Promise 的 then 方法(或者 catch 方法)是怎么实现的呢?
能手写一个 Promise 吗?
Promise 和 async/await 的区别是什么?
Promise 中有异步任务(例如 setTimeout 等)的执行顺序是什么样的呢?
为什么面试过程中 Promise 出现的频率这么高呢?
异步编程是 JavaScript 中的一个核心概念,与其他脚本编程语言相比,异步编程是一项让 JavaScript 速度更快的特性。JavaScript 是单线程的,这意味着它逐行执行程序。它也是异步的,这意味着如果我们的程序执行到达一个必须等待结果的代码块,它将继续经过这个正在等待的代码块,因此程序不会冻结执行,并且一旦该异步任务完成,我们的代码将通过使用回调来处理它正在等待的结果。如果回调太多,嵌套太深,Promise 确实可以解决这一痛点。其实上面的问题如果动手写过一次源码,基本就是都清楚了
接下来就根据 Promise 的特性来实现一下
大体结构如下:
第一步是需要根据使用实现构造函数;
第二步是实现原型方法 then,then 是核心逻辑,其他的方法都是对 then 方法的使用和完善;
下面我们就来一步步看看这个 Promise 的实现。
一、介绍 Promise
Promise 是 ES6 中进行异步编程的新解决方案(相对于单纯使用回调函数),具有三种状态:pending、rejected、resolved,状态的修改只能是 pending 到 rejected 或者 pending 到 resolved,且状态是不可逆的。它的使用这里就不多说啦,大致结构如下:
const p = new Promise((resolve, reject) => {resolve("success");
});
p.then((value) => {console.log("成功", value);},(reason) => {console.log("失败", reason);}
).catch((error) => {console.log("错误", error);
});
then 方法中有成功和失败的回调,catch 是捕获整个过程中产生的错误。
在这里需要注意一个问题,如果resolve("success");
是在一个异步中,例如定时器,then 方法并不是在定时器结束才绑定,而是直接绑定的,只不过成功和失败的回调是在状态修改以后才调用的,这个很重要,封装 then 方法的时候需要实现这一逻辑。
它的方法分为原型方法和构造函数方法,then 和 catch 为原型上的方法,即实例上可调用的方法,其它为构造函数的方法。现有的方法和解释给大家都列出来啦!
Promise.prototype.then 方法: (onResolved, onRejected) => {} (1) onResolved 函数: 成功的回调函数 (value) => {} (2) onRejected 函数: 失败的回调函数 (reason) => {} 说明: 指定用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调 返回一个新的 promise 对象
Promise.prototype.catch 方法: (onRejected) => {} (1) onRejected 函数: 失败的回调函数 (reason) => {} 说明: then()的语法糖, 相当于: then(undefined, onRejected)
Promise.resolve 方法: (value) => {} (1) value: 成功的数据或 promise 对象 说明: 返回一个成功/失败的 promise 对象
Promise.reject 方法: (reason) => {} (1) reason: 失败的原因 说明: 返回一个失败的 promise 对象
Promise.all 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明: 返回一个新的 promise, 接收一个 Promise 对象的集合,只有所有的 promise 都成功才成功, 只要有一个失败了就 直接失败
Promise.race 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明: 返回一个新的 promise, 接收一个 Promise 对象的集合,第一个完成的 promise 的结果状态就是最终的结果状态
Promise.any 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明: 返回一个新的 promise, 接收一个 Promise 对象的集合,当其中的一个 promise 成功,就返回那个成功的 promise 的值,如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和 AggregateError 类型的实例。
Promise.allSettled 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明:方法返回一个在所有给定的 promise 都已经
fulfilled
或rejected
后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise
的结果时,通常使用它。该方法为 ES2020 新增的特性,它能够返回所有任务的结果。
二、封装 Promise
根据 Promise 的使用可以确定需要封装的整体结构如下:
// 构造函数
function Promise(executor) {function resolve(data) {}function reject(data) {}executor(resolve, reject);
}//then方法
Promise.prototype.then = function (onResolved, onRejected) {};//catch方法
Promise.prototype.catch = function () {};Promise.resolve = function () {};
Promise.reject = function () {};
Promise.race = function () {};
Promise.any = function () {};
Promise.all = function () {};
Promise.allSettled = function () {};
new Promise()
有一个回调函数需要实现,且回调函数需要有两个参数,所以构造函数需要有一个参数executor
Promise 构造方法的实现如下:
// 构造函数
function Promise(executor) {this.promiseState = "pending";this.primiseResult = null;// 保存then的回调函数,使用数组主要是为了链式调用的场景,多个then方法的回调this.callbacks = [];const self = this;/*** 改变状态的三种方式* 1、resolve* 2、reject* 3、throw*/function resolve(data) {// 保证状态只能修改一次if (self.promiseState !== "pending") return;// 修改对象状态self.promiseState = "fulfilled";// 设置对象结果值self.primiseResult = data;// then方法的回调函数异步执行setTimeout(() => {// 状态改变触发回调函数的执行self.callbacks.forEach((item) => {item.onResolved(data);});});}function reject(data) {// 保证状态只能修改一次if (self.promiseState !== "pending") return;// 修改对象状态self.promiseState = "rejected";// 设置对象结果值self.primiseResult = data;// then方法的回调函数异步执行setTimeout(() => {// 状态改变触发回调函数的执行self.callbacks.forEach((item) => {item.onRejected(data);});});}// throw要改变状态 通过try...catch...try {executor(resolve, reject);} catch (e) {//catch方法的实现reject(e);}
}
改变 Promise 状态的三种方式:
resolve()
reject()
throw() 通过 try...catch...实现
上面的代码中兼容了对上面三种方法的处理,Promise 状态只能修改一次且不可逆,如果调用了 resolve(),然后再调用 reject(),只会执行前者,后者不执行;那么如何实现状态的不可逆修改呢?通过判断状态if(self.promiseState !== 'pending') return;
即保证每次都是从 pending 修改状态到失败或者成功。
new 完以后需要通过实例方法调用 then 和 catch 方法,所以下面是这两个方法的实现:
//then方法
Promise.prototype.then = function (onResolved, onRejected) {const self = this;// 【异常穿透】如果没有写失败的回调,这里需要补充上,并抛出一个错误if (typeof onRejected !== "function") {onRejected = (reason) => {throw reason;};}// 【值传递】if (typeof onResolved !== "function") {onResolved = (value) => value;}return new Promise((resolve, reject) => {function callback(type) {// 获取then回调函数的执行结果try {const result = type(self.primiseResult);if (result instanceof Promise) {// 返回结果是Promiseresult.then((v) => {resolve(v);},(r) => {reject(r);});} else {resolve(result);}} catch (e) {reject(e);}}if (this.promiseState === "fulfilled") {// then方法的回调函数异步执行setTimeout(() => {callback(onResolved);});}if (this.promiseState === "rejected") {// then方法的回调函数异步执行setTimeout(() => {callback(onRejected);});}// 异步处理,状态没有变更if (this.promiseState === "pending") {this.callbacks.push({onResolved: function () {callback(onResolved);},onRejected: function () {callback(onRejected);},});}});
};
then 方法中需要判断 pending 的情况,主要是因为状态变更有异步的可能,需要先存储 then 的回调函数,方便状态修改以后调用,将所有的异步回调存储到callbacks
,由于会有多个 then 方法链式调用,所以 callbacks 是数组,用于保存多个回调,且 then 方法的回调函数不是同步执行的,所以需要通过 setTimeout 放入另一个队列;
链式调用,涉及到 then 方法的返回,返回值必须是个 Promise 才能实现链式调用;成功的回调函数返回的结果也可能是 Promise;成功的回调函数返回的结果 考虑到 throw 的情况,还是要使用 try...catch...;中断 promise,返回一个 pending 状态的 promise;
// catch方法
// 需要处理异常穿透
Promise.prototype.catch = function (onRejected) {return this.then(undefined, onRejected);
};
catch 方法及异常穿透 catch 方法的功能 then 已经实现了,直接使用就可以,只是没有成功的处理函数;then 方法中没有写失败的回调函数,会默认添加一个失败的回调函数并抛出异常,最后统一由 catch 处理异常。
值传递:第一个回调函数不传也可以,我们会在 then 方法处理这种情况,如果检测到没有这个方法,就自动添加这个方法。
接下来是对构造函数的实现,之所以在 then 方法后面现实是因为下面这些方法的实现是基于上面的实现。resolve 方法快速创建 promise 对象的实现,所以可以直接调用封装好的 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);}});
};
接下来是 reject 方法的实现,传入什么都是返回失败,也是调用现有的方法,直接返回将状态修改为失败:
Promise.reject = function (value) {return new Promise((resolve, reject) => {reject(value);});
};
race 方法无论成功失败,只要最先返回的结果,只要有结果就返回:
Promise.race = function (promises) {return new Promise((resolve, reject) => {for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {// 最先返回的改变状态resolve(v);},(r) => {reject(r);});}});
};
all 方法的实现:其中一个 Promise 成功的时候不可以改变状态,只有全部成功才能改变状态;实现是使用一个计数器,当数量和promises
数量相同,且都成功了,就返回所有结果,失败直接改变状态结
// all方法
Promise.all = function (promises) {return new Promise((resolve, reject) => {let count = 0;let arr = [];for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {// 根据all的定义,不可以直接改变状态count++;//不用push是为了保证输出的顺序正常一一对应arr[i] = v;if (count === promises.length) {resolve(arr);}},(r) => {reject(r);});}});
};
any 方法实现:其中的一个 promise 成功,就返回那个成功的 promise 的值,失败返回一个AggregateError
类型的错误new AggregateError('AggregateError: All promises were rejected')
// any方法 其中的一个 promise 成功,就返回那个成功的promise的值,失败返回一个AggregateError类型的错误
Promise.any = function (promises) {let count = 0;return new Promise((resolve, reject) => {for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {resolve(v);},(r) => {count++;if (count === promises.length) {reject(new AggregateError("AggregateError: All promises were rejected"));}});}});
};
allSettled 方法是比较少知道的方法,有时候会在面试者被问到你如何将所有的成功失败结构都返回,下面就是答案:
// allSettled方法 所有结果都返回后显示每个结果的返回值
Promise.allSettled = function (promises) {return new Promise((resolve, reject) => {let arr = [];for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {arr[i] = { status: "fulfilled", value: v };if (arr.length === promises.length) {resolve(arr);}},(r) => {arr[i] = { status: "rejected", reason: r };if (arr.length === promises.length) {resolve(arr);}});}});
};
实现了上面的构造方法以后,可以发现,提供的方法如果在你的逻辑中不适用,也可以类比上面的方法实现自己想要的方法。
三、总结 Promise
一步一步实现下来,发现逻辑都是环环相扣的
由于需要状态的管理并且不可逆,所以需要有个变量来保存状态;
由于构造函数的参数(回调函数)可以改变状态,所以需要添加对应的方法来处理状态的修改;
又由于状态的可能是异步修改的,所以需要添加一个变量来保存 then 方法的回调函数;
由于 then 可以存在多个,所以保存回调函数的变量得是一个数组;
由于 then 可以链式调用,所以 then 方法必须返回一个 promise 对象;
其他方法也可以调用 then 方法,所以也需要返回一个 promise 对象;
由于 throw 也可以改变状态,所以处理需要使用 try...catch...实现状态的改变;
由于可能会存在 then 方法没有失败回调函数的情况,所以异常需要统一由 catch 方法收口;
由于 catch 方法可以再多个 then 方法之后,所以需要考虑异常穿透,将失败回调函数补充上并抛出异常;
其他方法的实现主要是在上面的基础上保证在特定的时期改变返回的 promise 的状态,有的是在第一次成功的时候返回成功(比如 any 方法);有的是在所有都成功的时候返回成功(比如 all 方法);有的是在第一结果返回的时候就返回,无论成功失败(比如 race 方法);有的是在所有结果都返回了以后就返回结果,无论成功失败(比如 allSettled 方法)。
四、扩展
async/await 也是异步编程的一种解决方案,他遵循的是 Generator 函数的语法糖,他拥有内置执行器,不需要额外的调用直接会自动执行并输出结果,它返回的是一个 Promise 对象。在涉及到比较复杂的业务场景,then 方法的调用会显得不太美观,但是 async/await 看起来就好很多,这一句代码执行完,才会执行下一句。
以上就是我对 promise 的学习和理解,如果有什么问题请大家指正。
最近组建了一个江西人的前端交流群,如果你是江西人可以加我微信 ruochuan12 私信 江西 拉你进群。
推荐阅读
整整4个月了,尽全力组织了源码共读活动~
我历时3年才写了10余篇源码文章,但收获了100w+阅读
老姚浅谈:怎么学JavaScript?
我在阿里招前端,该怎么帮你(可进面试群)
················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》10余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助1000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。
识别上方二维码加我微信、拉你进源码共读群
今日话题
略。分享、收藏、点赞、在看我的文章就是对我最大的支持~