找回密码
 会员注册
查看: 9|回复: 0

前端ES6之Promise实践应用与控制反转_UTF_8

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-30 17:29:56 | 显示全部楼层 |阅读模式
Promise 主要是为解决程序异步处理而生的,在现在的前端应用中无处不在,已然成为前端开发中最重要的技能点之一。它不仅解决了以前回调函数地狱嵌套的痛点,更重要的是它提供了更完整、更强大的异步解决方案。同时 Promise 也是前端面试中必不可少的考察点,考察内容可深可浅,因此熟练掌握它是每个前端开发者的必备能力。Promise 相对于 callback 模式的优势,网上的介绍文章已经多如牛毛,本文我将不再重点赘述。本文我主要会在介绍 Promise 的基础使用上,重点介绍其典型的场景应用,以及一些重难点场景分析,主要目的是提高对 Promise 的理解及对其灵活的运用能力。Promise 含义及基本介绍首先 Promise 也是一个类或构造函数,是 JS 原生提供的,和我们自定义的类一样,通过对它进行实例化后,来完成预期的异步任务处理。Promise 接受异步任务并立即执行,然后在任务完成后,将状态标注成最终结果(成功或失败)。Promise 有三种状态:初始化时,刚开始执行主体任务,这时它的初始状态时 pending(进行中) ;等到任务执行完成,这时根据成功或失败,分别对应状态 fulfilled(成功)和 rejected(失败) ,这时的状态就固定不能被改变了,即 Promise 状态是不可逆的。基本用法Promise 就是一个类,所以使用时,我们照常 new 一个实例即可。constmyPromise=newPromise((resolve,reject)=>{//这里是Promise主体,执行异步任务ajax('xxx',()=>{resolve('成功了');//或reject('失败了')})})上面创建好 Promise 实例后,里面的主体会立即执行,比如,如果是发送请求,则会立即把请求发出去,如果是定时器,则会立即启动计时。至于请求什么时候返回,我们就在返回成功的地方,通过 resolve() 将状态标注为成功即可,同时 resolve(data) 可以附带着返回数据。 然后在 then() 里面进行回调处理。constmyPromise=newPromise((resolve,reject)=>{//这里是Promise主体,执行异步任务ajax('xxx',()=>{resolve('成功了');})})myPromise.then((data)=>{//处理data数据})这里需要注意的是当初始化 Promise 实例时,主体代码是同步就开始执行了的,只有 then() 里面的回调处理才是异步的,因为它需要等待主体任务执行结束。技能考察时常常会通过分析执行顺序考察此处。 如下面的代码将输出 1、3、2。constmyPromise=newPromise((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 构建宏任务):constmyPromise=newPromise((resolve,reject)=>{//主体只有同步代码,则Promise状态会立马标注为成功console.log(1);resolve();}).then(()=>{console.log(2);})console.log(3);//最终输出为1、3、2Promise 异常处理方式一:通过 then() 的第 2 个参数constmyPromise=newPromise(...);myPromise.then(successCallback,errorCallback);这种方式能捕获到 promise 主体里面的异常,并执行 errorCallback。但是如果 Promise 主体里面没有异常,然后进入到 successCallback 里面发生了异常,此时将不会进入到 errorCallback。因此我们经常使用下面的方式二来处理异常。方式二:通过 catch() (常用方案)constmyPromise=newPromise(...);myPromise.then(successCallback).catch(errorCallback);这样不管是 Promise 主体,还是 successCallback 里面的出了异常,都会进入到 errorCallback。这里需要注意,按这种链式写法才正确,如果按下面的写法将会和方式一类似,不能按预期捕获,具体原因在后面的链式调用里面说明。constmyPromise=newPromise(...);myPromise.then(successCallback);myPromise.catch(errorCallback);方式三:try...catchtry catch 是传统的异常捕获方式,这里只能捕获同步代码的异常,并不能捕获异步异常,因此无法对 Promise 进行完整的异常捕获。链式调用熟悉 JQuery 的同学应该很了解链式调用,就是在调用了对象的一个方法后,此方法又返回了这个对象,从而可以继续在后面调用对象的方法。Promise 的链式调用,每次调用后,会返回一个新的 Promise 实例对象,从而可以继续 then()或者其他 API 调用,如上面的方式二异常处理中的 catch 就属于链式调用。constmyPromise=newPromise((resolve)=>{resolve(1)}).then((data)=>{returndata+1;})).then((data)=>{console.log(data)};//输出2这里需要注意的是,每次 then() 或者 catch() 后,返回的是一个新的 Promise,和上一次的 Promise 实例对象已经不是同一个引用了。而这个新的 Promise 实例对象包含了上一次 then 里面的结果,这也是为什么链式调用的 catch 才能捕获到上一次 then 里面的异常的原因。下面的代码非链式调用,每次 then 都是针对最初的 Promise 实例最后输出为 1。constmyPromise=newPromise((resolve)=>{resolve(1)})myPromise.then((data)=>{returndata+1;})romise.then((data)=>{console.log(data);})//输出1常用 API我再对一些常用 API 进行一下简单说明和介绍,Promise API 和大部分类一样,分为实例 API 或原型方法(即 new 出来的对象上的方法),和静态 API 或类方法(即直接通过类名调用,不需要 new)。注意实例 API 都是可以通过链式调用的。实例 API(原型方法)then()Promise 主体任务和在此之前的链式调用里的回调任务都成功的时候(即前面通过 resolve 标注状态后),进入本次 then() 回调。catch()Promise 主体任务和在此之前的链式调用里的出现了异常,并且在此之前未被捕获的时候(即前面通过 reject 标注状态或者出现 JS 原生报错没处理的时候),进入本次 catch()回调。finally()无论前面出现成功还是失败,最终都会执行这个方法(如果添加过)。比如某个任务无论成功还是失败,我们都希望能告诉用户任务已经执行结束了,就可以使用 finally()。静态 API(类方法)Promise.resolve()返回一个成功状态的 Promise 实例,一般常用于构建微任务,比如有个耗时操作,我们不希望阻塞主程序,就把它放到微任务去,如下输出 1、3、2,即 console.log(2) 将放到最后微任务去执行:console.log(1)romise.resolve().then(()=>{console.log(2);//作为微任务输出2})console.log(3)romise.reject()这个与 Promise.resolve 使用类似,返回一个失败状态的 Promise 实例。Promise.all()此方法接收一个数组为参数(准确说是可迭代参数),数组里面每一项都是一个单独的 Promise 实例,此方法返回一个 Promise 对象。这个返回的对象含义是数组中所有 Promise 都返回了(可失败可成功),返回 Promise 对象就算完成了。适用于需要并发执行任务时,比如同时发送多个请求。constp1=newPromise(...);constp2=newPromise(...);constp3=newPromise(...);constpAll=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]);//等同于constp1=newPromise(resolve=>resolve(1));constp2=newPromise(resolve=>resolve(2));constp3=newPromise(resolve=>resolve(3))romise.all([p1,p2,p3])romise.race()与 Promise.all() 类似,不过区别是 Promise.race 只要传入的 Promise 对象,有一个状态变化了,就会立即结束,而不会等待其他 Promise 对象返回。所以一般用于竞速的场景。接下来,来看看 Promise 具体的使用场景。Promise 最佳实践介绍Promise 的 API 不多,使用也不复杂,简单场景一看就明白,不过对于一些复杂的代码模块,不够熟悉的同学就会感觉比较绕。比如这些实际应用中的经验。异步 Promise 化的两个关键实际应用中,我们尽量将所有异步操作进行 Promise 的封装,方便其他地方调用。放弃以前的 callback 写法,比如我们封装了一个类 classA,里面需要有一些准备工作才能被外界使用,以前我们可能会提供 ready(callback) 方法,那么现在就可以这样 ready().then()。另外,一般开发中,尽量将 new Promise 的操作封装在内部,而不是在业务层去实例化。如下面代码://封装functiongetData(){constpromise=newPromise((resolve,reject)=>{ajax(xxx,(d)=>{resolve(d);})});returnpromise}//使用getData().then((data)=>{console.log(data)})其实处理和封装异步任务关键就是两件事定义异步任务的执行内容。如发一个请求、设一个定时器、读取一个文件等;指出异步任务结束的时机。如请求返回时机、定时器结束的时机、文件读取完成的时机,其实就是触发回调的时机。当通过 new Promise 初始化实例的时候,就定义了异步任务的执行内容,即 Promise 主体。然后 Promise 给我们两个函数 resolve 和 reject 来让我们明确指出任务结束的时机,也就是告诉 Promise 执行的内容和结束的时机就行了,不用像 callback 那样,需要把处理过程也嵌套写在里面,而是在原来 callback 的地方调用一下 resolve(成功)或 reject(失败)来标识任务结束了。在实际开发中,不管业务模块或者老代码多么复杂,只需要抓住上述两点去进行改造,就能正确地将所有异步代码进行 Promise 化。 所有异步甚至同步逻辑都可以 Promise 化,只要抓住 任务内容和 任务结束时机这两点就可很清晰的来完成封装。如何避免冗余封装?现在很多类库已经支持返回 Promise 实例了,尽量避免在外面重复包装,所以在使用时仔细看官方说明,有的库既支持 callback 形式,也支持 Promise 形式。下面代码为冗余封装:functiongetData(){returnnewPromise((resolve)=>{axios.get(url).then((data)=>{resolve(data)})})}另一个案例就是,有时我们会需要构建微任务或者将同步执行的结果数据,以 Promise 的形式返回给业务,会容易写成下面的冗余写法:functiongetData(){returnnewPromise((resolve)=>{consta=1;constb=2;constc=a+b;resolve(c);})}优化写法应该如下,即用 Promise.resolve 快速构建一个 Promise 对象:functiongetData(){consta=1;constb=2;constc=a+b;returnPromise.resolve(c);}异常处理前面 API 的介绍中已经有说明,尽量通过 catch() 去捕获 Promise 异常,需要说明的是,一旦被 catch 捕获过的异常,将不会再往外部传递,除非在 catch 中又触发了新的异常。如下面代码,第一个异常被捕获后,就返回了一个新的 Promise,这个 Promise 对象没有异常,将会进入后面的 then() 逻辑:constp=newPromise((resolve,reject)=>{reject('异常啦');//或者通过thrownewError()跑出异常}).catch((err)=>{console.log('捕获异常啦');//进入}).catch(()=>{console.log('还有异常吗');//不进入}).then(()=>{console.log('成功');//进入})如果 catch 里面在处理异常时,又发生了新的异常,将会继续往外冒,这个时候我们不可能无止尽的在后面添加 catch 来捕获,所以 Promise有一个小的缺点就是最后一个 catch 的异常没办法捕获(当然实际出现异常的可能性很低,基本不造成什么影响)。使用 async await实际使用中,我们一般通过 async await 来配合 Promise 使用,这样可以让代码可读性更强,彻底没有"回调"的痕迹了。asyncfunctiongetData(){constdata=awaitaxios.get(url);returndata;}//等效于functiongetData(){returnaxios.get(url).then((data)=>{returndata});}对 async await 很多人都会用,但要注意几个非常重要的点。await 同一行后面的内容对应 Promise 主体内容,即同步执行的await 下一行的内容对应 then()里面的内容,是异步执行的await 同一行后面应该跟着一个 Promise 对象,如果不是,需要转换(如果是常量会自动转换)async 函数的返回值还是一个 Promise 对象比如下面写法就是不正确的:asyncfunctiongetData(){//await不认识后面的setTimeout,不知道何时返回constdata=awaitsetTimeout(()=>{return;},3000)console.log('3秒到了')}正确写法是:asyncfunctiongetData(){constdata=awaitnewPromise((resolve)=>{setTimeout(()=>{resolve();},3000)})console.log('3秒到了')}Promise 高级应用提前预加载应用有这样一个场景:页面的数据量较大,通过缓存类将数据缓存在了本地,下一次可以直接使用缓存,在一定数据规模时,本地的缓存初始化和读取策略也会比较耗时。这个时候我们可以继续等待缓存类初始完成并读取本地数据,也可以不等待缓存类,而是直接提前去后台请求数据。两种方法最终谁先返回的时间不确定。那么为了让我们的数据第一时间准备好,让用户尽可能早地看到页面,我们可以通过 Promise 来做加载优化。策略是页面加载后,立马调用 Promise 封装的后台请求,去后台请求数据。同时初始化缓存类并调用 Promise 封装的本地读取数据。最后在显示数据的时候,看谁先返回用谁的。中断场景应用实际应用中,还有这样一种场景:我们正在发送多个请求用于请求数据,等待完成后将数据插入到不同的 dom 元素中,而如果在中途 dom 元素被销毁了(比如 react 在 useEffect 中请求的数据时,组件销毁),这时就可能会报错。因此我们需要提前中断正在请求的 Promise,不让其进入到 then 中执行回调。useEffect(()=>{letdataPromise=newPromise(...);letdata=awaitdataPromise();//TODO接下来处理data,此时本组件可能已经销毁了,dom也不存在了,所以需要在下面对Promise进行中断return(()=>{//TODO组件销毁时,对dataPromise进行中断或取消})});我们可以对生成的 Promise 对象进行再一次包装,返回一个新的 Promise 对象,而新的对象上被我们增加了 cancel 方法,用于取消。这里的原理就是在 cancel 方法里面去阻止 Promise 对象执行 then()方法。下面构造了一个 cancelPromise 用于和原始 Promise 竞速,最终返回合并后的 Promise,外层如果调用了 cancel 方法,cancelPromise 将提前结束,整个 Promise 结束。functiongetPromiseWithCancel(originPromise){letcancel=(v)=>{};letisCancel=false;constcancelPromise=newPromise(function(resolve,reject){cancel=e=>{isCancel=true;reject(e);};});constgroupPromise=Promise.race([originPromise,cancelPromise]).catch(e=>{if(isCancel){//主动取消时,不触发外层的catchreturnnewPromise(()=>{});}else{returnPromise.reject(e);}});returnObject.assign(groupPromise,{cancel});}//使用如下constoriginPromise=axios.get(url);constpromiseWithCancel=getPromiseWithCancel(originPromise);promiseWithCancel.then((data)=>{console.log('渲染数据',data);});promiseWithCancel.cancel();//取消Promise,将不会再进入then()渲染数据Promise 深入理解之控制反转熟悉了 Promise 的基本运用后,我们再来深入点理解。Promise 和 callback 还有个本质区别,就是控制权反转。callback 模式下,回调函数是由业务层传递给封装层的,封装层在任务结束时执行了回调函数。而 Promise 模式下,业务层并没有把回调函数直接传递给封装层( Promise 对象内部),封装层在任务结束时也不知道要做什么回调,只是通过 resolve 或 reject 来通知到 业务层,从而由业务层自己在 then() 或 reject() 里面去控制自己的回调执行。这里可能理解起来有点绕,换种等效的简单理解:我们知道函数一般是分定义+调用步骤的,先定义,后调用。谁调用了函数,就表示谁在控制这个函数的执行。那么我们来看 callback 模式下,业务层将回调函数的定义传给了封装层,封装层在内部完成了回调函数的调用执行,业务层并没有调用回调函数,甚至业务层都看不到其调用代码,所以回调函数的执行控制权在封装层。而 Promise 模式下,回调函数的调用执行是在 then() 里面完成的,是由业务层发起的,业务层不仅能看到回调函数的调用代码,也能修改,因此回调函数的控制权在业务层。手动实现 Promise 类的思路现在我们已经熟悉了 Promise 的详细使用方式,假设让你回到 Promise 类出现之前,那时的 ES6 还没出现,你为了淘汰 callback 的回调写法,准备自己写一个 Promise 类,你会怎么做?其实这就是常见面试手写 Promise 题目。我们只要抓住 Promise 的一些特点和关键点就能比较顺利实现。首先 Promise 是一个类,构造函数接收参数是一个函数,而这个函数的参数是 resolve 和 reject 两个内部函数,也就是我们需要构建 resolve 和 reject 传给它,同时让它立即执行。另外咱这个类是有三种状态及 then 和 catch 等方法。根据这些就能快速先把类框架创建好。classMyPromise(){constructor(fun){this.status='pending';//pending、fulfilled、rejectedfun(this.resolve,this.reject);//立即执行主体函数,参数函数可能需要bind(this)}resolve(){}//定义resolve,内容待定reject(){}//定义reject,内容待定then(){}catch(){}}有了雏形之后,再根据对 Promise 的理解逐步完善即可,如 resolve 和 reject 里面我们肯定是要去修改 status 状态的; 而 then() 里面我们需要接收并保存传进来的回调等等。 完整案例可在网上搜索,重点是理解它的实现思路。总结今天我们对 Promise 进行了基本 API 介绍,然后重点对其实际应用进行了介绍和解析。相信通过本文的学习,可以提升你对 Promise 的理解和运用能力。同时文中的一些实际场景举例是非常典型的应用场景,比如 async await 和手写 Promise 是很容易被考察的点。并且考察方式变化很多,万变不离其宗,抓住文中重点内容,做到举一反三不是问题。最后可以看一个有点难度的 Promise 执行顺序分析题目:functionpromise2(){returnnewPromise((resolve)=>{console.log('promise2start');resolve();})}functionpromise3(){returnnewPromise((resolve)=>{console.log('promise3start');resolve();})}functionpromise4(){returnnewPromise((resolve)=>{console.log('promise4start');resolve();}).then(()=>{console.log('promise4end');})}asyncfunctionasyncFun(){console.log('async1start');awaitpromise2();console.log('async1inner');awaitpromise3();console.log('async1end');}setTimeout(()=>{console.log('setTimeoutstart');promise1();console.log('setTimeoutend');},0);asyncFun();promise4();console.log('scriptend');点击上方关注我们下期再见
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-14 19:56 , Processed in 0.741010 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表