Promise 主要是为解决程序异步处理而生的,在现在的前端应用中无处不在,已然成为前端开发中最重要的技能点之一。它不仅解决了以前回调函数地狱嵌套的痛点,更重要的是它提供了更完整、更强大的异步解决方案。
同时 Promise 也是前端面试中必不可少的考察点,考察内容可深可浅,因此熟练掌握它是每个前端开发者的必备能力。
Promise 相对于 callback 模式的优势,网上的介绍文章已经多如牛毛,本文我将不再重点赘述。本文我主要会在介绍 Promise 的基础使用上,重点介绍其典型的场景应用,以及一些重难点场景分析,主要目的是提高对 Promise 的理解及对其灵活的运用能力。
首先 Promise 也是一个类或构造函数,是 JS 原生提供的,和我们自定义的类一样,通过对它进行实例化后,来完成预期的异步任务处理。
Promise 接受异步任务并立即执行,然后在任务完成后,将状态标注成最终结果(成功或失败)。
Promise 有三种状态:初始化时,刚开始执行主体任务,这时它的初始状态时 pending(进行中) ;等到任务执行完成,这时根据成功或失败,分别对应状态 fulfilled(成功)和 rejected(失败) ,这时的状态就固定不能被改变了,即 Promise 状态是不可逆的。
Promise 就是一个类,所以使用时,我们照常 new 一个实例即可。
const myPromise = new Promise((resolve, reject) => {
// 这里是 Promise 主体,执行异步任务
ajax('xxx', () => {
resolve('成功了'); // 或 reject('失败了')
})
})
上面创建好 Promise 实例后,里面的主体会立即执行,比如,如果是发送请求,则会立即把请求发出去,如果是定时器,则会立即启动计时。至于请求什么时候返回,我们就在返回成功的地方,通过 resolve() 将状态标注为成功即可,同时 resolve(data) 可以附带着返回数据。 然后在 then() 里面进行回调处理。
const myPromise = new Promise((resolve, reject) => {
// 这里是 Promise 主体,执行异步任务
ajax('xxx', () => {
resolve('成功了');
})
})
myPromise.then((data) => {
// 处理 data 数据
})
这里需要注意的是当初始化 Promise 实例时,主体代码是同步就开始执行了的,只有 then() 里面的回调处理才是异步的,因为它需要等待主体任务执行结束。技能考察时常常会通过分析执行顺序考察此处。 如下面的代码将输出 1、3、2。
const myPromise = new Promise((resolve, reject) => {
// 这里是 Promise 主体,执行异步任务
console.log(1);
ajax('xxx', () => {
resolve('成功了');
})
}).then(() => {
console.log(2);
})
console.log(3);// 最终输出 1、3、2
如果我们在调用 then() 之前,Promise 主体里的异步任务已经执行完了,即 Promise 的状态已经标注为成功了。那么我们调用 then 的时候,并不会错过,还是会执行。但需要记着,即使主体的异步任务早就执行完了,then() 里面的回调永远是放到微任务里面异步执行的,而不是立马执行。
比如我们在主体里面仅执行一块同步代码,从而不需要等待,下面代码 then() 将依然最后输出。因此我们也常常利用这种方式构建微任务(相对应的利用 setTimeout 构建宏任务):
const myPromise = new Promise((resolve, reject) => {
// 主体只有同步代码,则 Promise 状态会立马标注为成功
console.log(1);
resolve();
}).then(() => {
console.log(2);
})
console.log(3);
// 最终输出为 1、3、2
const myPromise = new Promise(...);
myPromise.then(successCallback, errorCallback);
这种方式能捕获到 promise 主体里面的异常,并执行 errorCallback。但是如果 Promise 主体里面没有异常,然后进入到 successCallback 里面发生了异常,此时将不会进入到 errorCallback。因此我们经常使用下面的方式二来处理异常。
const myPromise = new Promise(...);
myPromise.then(successCallback).catch(errorCallback);
这样不管是 Promise 主体,还是 successCallback 里面的出了异常,都会进入到 errorCallback。这里需要注意,按这种链式写法才正确,如果按下面的写法将会和方式一类似,不能按预期捕获,具体原因在后面的链式调用里面说明。
const myPromise = new Promise(...);
myPromise.then(successCallback);
myPromise.catch(errorCallback);
try catch 是传统的异常捕获方式,这里只能捕获同步代码的异常,并不能捕获异步异常,因此无法对 Promise 进行完整的异常捕获。
熟悉 JQuery 的同学应该很了解链式调用,就是在调用了对象的一个方法后,此方法又返回了这个对象,从而可以继续在后面调用对象的方法。Promise 的链式调用,每次调用后,会返回一个新的 Promise 实例对象,从而可以继续 then()或者其他 API 调用,如上面的方式二异常处理中的 catch 就属于链式调用。
V
这里需要注意的是,每次 then() 或者 catch() 后,返回的是一个新的 Promise,和上一次的 Promise 实例对象已经不是同一个引用了。而这个新的 Promise 实例对象包含了上一次 then 里面的结果,这也是为什么链式调用的 catch 才能捕获到上一次 then 里面的异常的原因。
下面的代码非链式调用,每次 then 都是针对最初的 Promise 实例最后输出为 1。
const myPromise = new Promise((resolve) => {
resolve(1)
})
myPromise.then((data) => {
return data + 1;
})
romise.then((data) => {
console.log(data);
})
// 输出 1
我再对一些常用 API 进行一下简单说明和介绍,Promise API 和大部分类一样,分为实例 API 或原型方法(即 new 出来的对象上的方法),和静态 API 或类方法(即直接通过类名调用,不需要 new)。注意实例 API 都是可以通过链式调用的。
实例 API(原型方法)
Promise 主体任务和在此之前的链式调用里的回调任务都成功的时候(即前面通过 resolve 标注状态后),进入本次 then() 回调。
Promise 主体任务和在此之前的链式调用里的出现了异常,并且在此之前未被捕获的时候(即前面通过 reject 标注状态或者出现 JS 原生报错没处理的时候),进入本次 catch()回调。
无论前面出现成功还是失败,最终都会执行这个方法(如果添加过)。比如某个任务无论成功还是失败,我们都希望能告诉用户任务已经执行结束了,就可以使用 finally()。
静态 API(类方法)
返回一个成功状态的 Promise 实例,一般常用于构建微任务,比如有个耗时操作,我们不希望阻塞主程序,就把它放到微任务去,如下输出 1、3、2,即 console.log(2) 将放到最后微任务去执行:
console.log(1);
Promise.resolve().then(() => {
console.log(2); // 作为微任务输出 2
})
console.log(3);
这个与 Promise.resolve 使用类似,返回一个失败状态的 Promise 实例。
此方法接收一个数组为参数(准确说是可迭代参数),数组里面每一项都是一个单独的 Promise 实例,此方法返回一个 Promise 对象。这个返回的对象含义是数组中所有 Promise 都返回了(可失败可成功),返回 Promise 对象就算完成了。适用于需要并发执行任务时,比如同时发送多个请求。
const p1 = new Promise(...);
const p2 = new Promise(...);
const p3 = new Promise(...);
const pAll = Promise.all([p1, p2, p3]);
pAll.then((list) => {
// p1,p2,p3 都成功了即都 resolve 了,会进入这里;
// list 按顺序为 p1,p2,p3 的 resolve 携带的返回值
}).catch(() => {
// p1,p2,p3 有至少一个失败,其他成功,就会进入这里;
})
注意 Promise.all 是所有传入的值都返回状态了,才会最终进入 then 或 catch 回调。
Promise 的参数也可以如下常量,它会转换成立即完成的 Promise 对象:
Promise.all([1, 2, 3]);
// 等同于
const p1 = new Promise(resolve => resolve(1));
const p2 = new Promise(resolve => resolve(2));
const p3 = new Promise(resolve => resolve(3));
Promise.all([p1, p2, p3]);
与 Promise.all() 类似,不过区别是 Promise.race 只要传入的 Promise 对象,有一个状态变化了,就会立即结束,而不会等待其他 Promise 对象返回。所以一般用于竞速的场景。
接下来,来看看 Promise 具体的使用场景。
Promise 的 API 不多,使用也不复杂,简单场景一看就明白,不过对于一些复杂的代码模块,不够熟悉的同学就会感觉比较绕。比如这些实际应用中的经验。
实际应用中,我们尽量将所有异步操作进行 Promise 的封装,方便其他地方调用。放弃以前的 callback 写法,比如我们封装了一个类 classA,里面需要有一些准备工作才能被外界使用,以前我们可能会提供 ready(callback) 方法,那么现在就可以这样 ready().then()。
另外,一般开发中,尽量将 new Promise 的操作封装在内部,而不是在业务层去实例化。
如下面代码:
// 封装
function getData(){
const promise = new Promise((resolve,reject)=>{
ajax(xxx, (d) => {
resolve(d);
})
});
return promise
}
// 使用
getData().then((data)=>{
console.log(data)
})
其实处理和封装异步任务关键就是两件事
当通过 new Promise 初始化实例的时候,就定义了异步任务的执行内容,即 Promise 主体。然后 Promise 给我们两个函数 resolve 和 reject 来让我们明确指出任务结束的时机,也就是告诉 Promise 执行的内容和结束的时机就行了,不用像 callback 那样,需要把处理过程也嵌套写在里面,而是在原来 callback 的地方调用一下 resolve(成功)或 reject(失败)来标识任务结束了。
在实际开发中,不管业务模块或者老代码多么复杂,只需要抓住上述两点去进行改造,就能正确地将所有异步代码进行 Promise 化。 所有异步甚至同步逻辑都可以 Promise 化,只要抓住 任务内容和 任务结束时机这两点就可很清晰的来完成封装。
现在很多类库已经支持返回 Promise 实例了,尽量避免在外面重复包装,所以在使用时仔细看官方说明,有的库既支持 callback 形式,也支持 Promise 形式。
下面代码为冗余封装:
function getData() {
return new Promise((resolve) => {
axios.get(url).then((data) => {
resolve(data)
})
})
}
另一个案例就是,有时我们会需要构建微任务或者将同步执行的结果数据,以 Promise 的形式返回给业务,会容易写成下面的冗余写法:
function getData() {
return new Promise((resolve) => {
const a = 1;
const b = 2;
const c = a + b;
resolve(c);
})
}
优化写法应该如下,即用 Promise.resolve 快速构建一个 Promise 对象:
function getData() {
const a = 1;
const b = 2;
const c = a + b;
return Promise.resolve(c);
}
前面 API 的介绍中已经有说明,尽量通过 catch() 去捕获 Promise 异常,需要说明的是,一旦被 catch 捕获过的异常,将不会再往外部传递,除非在 catch 中又触发了新的异常。
如下面代码,第一个异常被捕获后,就返回了一个新的 Promise,这个 Promise 对象没有异常,将会进入后面的 then() 逻辑:
const p = new Promise((resolve, reject) => {
reject('异常啦'); // 或者通过 throw new Error() 跑出异常
}).catch((err) => {
console.log('捕获异常啦'); // 进入
}).catch(() => {
console.log('还有异常吗'); // 不进入
}).then(() => {
console.log('成功'); // 进入
})
如果 catch 里面在处理异常时,又发生了新的异常,将会继续往外冒,这个时候我们不可能无止尽的在后面添加 catch 来捕获,所以 Promise 有一个小的缺点就是最后一个 catch 的异常没办法捕获(当然实际出现异常的可能性很低,基本不造成什么影响)。
实际使用中,我们一般通过 async await 来配合 Promise 使用,这样可以让代码可读性更强,彻底没有"回调"的痕迹了。
async function getData() {
const data = await axios.get(url);
return data;
}
// 等效于
function getData() {
return axios.get(url).then((data) => {
return data
});
}
对 async await 很多人都会用,但要注意几个非常重要的点。
比如下面写法就是不正确的:
async function getData() {
// await 不认识后面的 setTimeout,不知道何时返回
const data = await setTimeout(() => {
return;
}, 3000)
console.log('3 秒到了')
}
正确写法是:
async function getData() {
const data = await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 3000)
})
console.log('3 秒到了')
}
有这样一个场景:页面的数据量较大,通过缓存类将数据缓存在了本地,下一次可以直接使用缓存,在一定数据规模时,本地的缓存初始化和读取策略也会比较耗时。这个时候我们可以继续等待缓存类初始完成并读取本地数据,也可以不等待缓存类,而是直接提前去后台请求数据。两种方法最终谁先返回的时间不确定。那么为了让我们的数据第一时间准备好,让用户尽可能早地看到页面,我们可以通过 Promise 来做加载优化。
策略是页面加载后,立马调用 Promise 封装的后台请求,去后台请求数据。同时初始化缓存类并调用 Promise 封装的本地读取数据。最后在显示数据的时候,看谁先返回用谁的。
实际应用中,还有这样一种场景:我们正在发送多个请求用于请求数据,等待完成后将数据插入到不同的 dom 元素中,而如果在中途 dom 元素被销毁了(比如 react 在 useEffect 中请求的数据时,组件销毁),这时就可能会报错。因此我们需要提前中断正在请求的 Promise,不让其进入到 then 中执行回调。
useEffect(() => {
let dataPromise = new Promise(...);
let data = await dataPromise();
// TODO 接下来处理 data,此时本组件可能已经销毁了,dom 也不存在了,所以需要在下面对 Promise 进行中断
return (() => {
// TODO 组件销毁时,对 dataPromise 进行中断或取消
})
});
我们可以对生成的 Promise 对象进行再一次包装,返回一个新的 Promise 对象,而新的对象上被我们增加了 cancel 方法,用于取消。这里的原理就是在 cancel 方法里面去阻止 Promise 对象执行 then()方法。
下面构造了一个 cancelPromise 用于和原始 Promise 竞速,最终返回合并后的 Promise,外层如果调用了 cancel 方法,cancelPromise 将提前结束,整个 Promise 结束。
function getPromiseWithCancel(originPromise) {
let cancel = (v) => {};
let isCancel = false;
const cancelPromise = new Promise(function (resolve, reject) {
cancel = e => {
isCancel = true;
reject(e);
};
});
const groupPromise = Promise.race([originPromise, cancelPromise])
.catch(e => {
if (isCancel) {
// 主动取消时,不触发外层的 catch
return new Promise(() => {});
} else {
return Promise.reject(e);
}
});
return Object.assign(groupPromise, { cancel });
}
// 使用如下
const originPromise = axios.get(url);
const promiseWithCancel = getPromiseWithCancel(originPromise);
promiseWithCancel.then((data) => {
console.log('渲染数据', data);
});
promiseWithCancel.cancel(); // 取消 Promise,将不会再进入 then() 渲染数据
熟悉了 Promise 的基本运用后,我们再来深入点理解。Promise 和 callback 还有个本质区别,就是控制权反转。
callback 模式下,回调函数是由业务层传递给封装层的,封装层在任务结束时执行了回调函数。
而 Promise 模式下,业务层并没有把回调函数直接传递给封装层( Promise 对象内部),封装层在任务结束时也不知道要做什么回调,只是通过 resolve 或 reject 来通知到 业务层,从而由业务层自己在 then() 或 reject() 里面去控制自己的回调执行。
这里可能理解起来有点绕,换种等效的简单理解:我们知道函数一般是分定义 + 调用步骤的,先定义,后调用。谁调用了函数,就表示谁在控制这个函数的执行。
那么我们来看 callback 模式下,业务层将回调函数的定义传给了封装层,封装层在内部完成了回调函数的调用执行,业务层并没有调用回调函数,甚至业务层都看不到其调用代码,所以回调函数的执行控制权在封装层。
而 Promise 模式下,回调函数的调用执行是在 then() 里面完成的,是由业务层发起的,业务层不仅能看到回调函数的调用代码,也能修改,因此回调函数的控制权在业务层。
现在我们已经熟悉了 Promise 的详细使用方式,假设让你回到 Promise 类出现之前,那时的 ES6 还没出现,你为了淘汰 callback 的回调写法,准备自己写一个 Promise 类,你会怎么做?
其实这就是常见面试手写 Promise 题目。我们只要抓住 Promise 的一些特点和关键点就能比较顺利实现。
首先 Promise 是一个类,构造函数接收参数是一个函数,而这个函数的参数是 resolve 和 reject 两个内部函数,也就是我们需要构建 resolve 和 reject 传给它,同时让它立即执行。另外咱这个类是有三种状态及 then 和 catch 等方法。根据这些就能快速先把类框架创建好。
class MyPromise () {
constructor (fun) {
this.status = 'pending'; // pending、fulfilled、rejected
fun(this.resolve, this.reject); // 立即执行主体函数,参数函数可能需要 bind(this)
}
resolve() {} // 定义 resolve,内容待定
reject() {} // 定义 reject,内容待定
then() {}
catch() {}
}
有了雏形之后,再根据对 Promise 的理解逐步完善即可,如 resolve 和 reject 里面我们肯定是要去修改 status 状态的; 而 then() 里面我们需要接收并保存传进来的回调等等。 完整案例可在网上搜索,重点是理解它的实现思路。
今天我们对 Promise 进行了基本 API 介绍,然后重点对其实际应用进行了介绍和解析。相信通过本文的学习,可以提升你对 Promise 的理解和运用能力。
同时文中的一些实际场景举例是非常典型的应用场景,比如 async await 和手写 Promise 是很容易被考察的点。并且考察方式变化很多,万变不离其宗,抓住文中重点内容,做到举一反三不是问题。
最后可以看一个有点难度的 Promise 执行顺序分析题目:
function promise2() {
return new Promise((resolve) => {
console.log('promise2 start');
resolve();
})
}
function promise3() {
return new Promise((resolve) => {
console.log('promise3 start');
resolve();
})
}
function promise4() {
return new Promise((resolve) => {
console.log('promise4 start');
resolve();
}).then(() => {
console.log('promise4 end');
})
}
async function asyncFun() {
console.log('async1 start');
await promise2();
console.log('async1 inner');
await promise3();
console.log('async1 end');
}
setTimeout(() => {
console.log('setTimeout start');
promise1();
console.log('setTimeout end');
}, 0);
asyncFun();
promise4();
console.log('script end');
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/36he_7HHuYNKyKS53B8nFQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。