Discuz! Board

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz

异步编程发展历程之Promise的演进史

查看数: 1126 | 评论数: 3 | 收藏 0
关灯 | 提示:支持键盘翻页<-左 右->
    组图打开中,请稍候......
发布时间: 2022-2-15 22:46

正文摘要:

https://e.naixuejiaoyu.com/detai ... bdb25/4?fromH5=true

回复

Qter 发表于 2022-2-18 22:23:25
面试宝典(必会面试高频)3(5)(1).docx
面试题.md
面试题专题.doc
外派阿里前端面试题库(1).doc
中高级面试总结.doc


链接:https://pan.baidu.com/s/1_ggvAL90AhGawb5YF7lPnw
提取码:fire

Qter 发表于 2022-2-15 22:55:28
FEP02 异步编程发展历程之Promise的演进史

介绍JavaScript是⼀⻔典型的异步编程脚本语⾔,在编程过程中会⼤量的出现异步代码的编写,在JS的整个发展历程中,对异步编程的处理⽅式经历了很多个时代,其中最典型也是现今使⽤最⼴泛的时代,就是Promise对象处理异步编程的时代。那么什么是Promise对象呢?Promise是ES6版本提案中实现的异步处理⽅式,对象代表了未来将要发⽣的事件,⽤来传递异步操作的消息。


为什么使⽤romise对象


举个栗⼦:在过去的编程中JavaScript的主要异步处理⽅式,是采⽤回调函数的⽅式来进⾏处理,想要保证n个步骤的异步编程有序进⾏,会出现如下的代码(以setTimeout为栗⼦)参考上⾯的代码,如果分3秒每间隔1秒运⾏1个任务,这三个任务必须按时间顺序执⾏,并且每个下⼀秒触发前都要先拿到上⼀秒运⾏的结果,那么我们不得不将代码编写为以上案例代码。该写法主要是为了保证代码的严格顺序要求,这样就避免不了⼤量的逻辑在回调函数中不停的进⾏嵌套,这也是我们经常听说的“回调地狱”。再举个栗⼦:在编程中setTimeout的“栗⼦”其实使⽤场景极少,在前端开发过程中使⽤最多的异步流程就是AJAX请求,当系统中要求某个⻚⾯的n个接⼝保证有序调⽤的情况下就会出现下⾯的情况:setTimeout(function(){ //第⼀秒后执⾏的逻辑 console.log('第⼀秒之后发⽣的事情') setTimeout(function(){ //第⼆秒后执⾏的逻辑 console.log('第⼆秒之后发⽣的事情') setTimeout(function(){ //第三秒后执⾏的逻辑 console.log('第三秒之后发⽣的事情') },1000) },1000)},1000)//获取类型数据$.ajax({ url:'/***', success:function(res){ var xxId = res.id //获取该类型的数据集合,必须等待回调执⾏才能进⾏下⼀步 $.ajax({ url:'/***',这种情况在很多⼈的代码中都出现过,如果流程复杂化,在⽹络请求中继续夹杂其他的异步流程,那么这样的代码就会变得难以维护了。其他的“栗⼦”诸如Node中的原始fs模块,操作⽂件系统这种就不举了,所以之所以在ECMA提案中出现Promise解决⽅案,就是因为此类代码导致了JS在开发过程中遇到的实际问题:【回调地狱】。其实解决回调地狱的⽅式还有其他⽅案,本节我们不多做介绍中间的过渡⽅案,核⼼介绍Promise流程控制对象,因为他是解决回调地狱的⾮常好的⽅案。使⽤romise如何解决异步控制问题前⾯的章节仅仅是抛出了问题,并没有针对问题作出⼀个合理的回答。下⾯阐述⼀下如何使⽤romise对象解决回调地狱问题。在阐述之前,我们先对Promise做⼀个简单的介绍:Promise对象的主要⽤途是通过链式调⽤的结构,将原本回调嵌套的异步处理流程,转化成“对象.then().then()...”的链式结构,这样虽然仍离不开回调函数,但是将原本的回调嵌套结构,转化成了连续调⽤的结构,这样就可以在阅读上编程上下左右结构的异步执⾏流程了。接下来看⼀段代码,还是以setTimout为“栗⼦”我们改造第⼀个案例: data:{ xxId:xxId,//使⽤上⼀个请求结果作为参数调⽤下⼀个接⼝ }, success:function(res1){ //得到指定类型集合 ... } }) }})//使⽤romise拆解的setTimeout流程控制var p = new Promise(function(resolve){ setTimeout(function(){ resolve() },1000)})p.then(function(){ //第⼀秒后执⾏的逻辑 console.log('第⼀秒之后发⽣的事情') return new Promise(function(resolve){ setTimeout(function(){ resolve() },1000) })}).then(function(){ //第⼆秒后执⾏的逻辑 console.log('第⼆秒之后发⽣的事情') return new Promise(function(resolve){ setTimeout(function(){ resolve()结合代码案例我们发现使⽤了Promise后的代码,将原来的3个setTimeout的回调嵌套,拆解成了三次then包裹的回调函数,按照上下顺序进⾏编写。这样我们从视觉上就可以按照⼈类的从上到下从左到右的线性思维来阅读代码,这样很容易能查看这段代码的执⾏流程,代价是代码的编写量增加了接近1倍。


Promise介绍


从上⾯的案例介绍得知Promise的作⽤是解决“回调地狱”,他的解决⽅式是将回调嵌套拆成链式调⽤,这样便可以按照上下顺序来进⾏异步代码的流程控制。那么Promise是如何实现这个能⼒的呢?Promise的结构Promise对象是⼀个JavaScript对象,在⽀持ES6语法的运⾏环境中作为全局对象提供,他的初始化⽅式如下:关于回调函数这⾥涉及到⼀个概念:JavaScript语⾔中,有⼀个特殊的函数叫做回调函数。回调函数的特点是把函数作为变量看待,由于JavaScript变量可以作为函数的形参并且函数可以通过声明变量的⽅式匿名创建,所以我们可以在定义函数时将⼀个函数的参数当作函数来执⾏,进⽽在调⽤时在参数的位置编写⼀个执⾏函数,代码如下:上⾯的代码结构,就是JavaScript中典型的回调函数结构。按照我们在事件循环中介绍的JavaScript函数运⾏机制,会发现其实回调函数本身是同步代码,这是⼀个需要【重点理解】的知识点。通常在编写JavaScript代码时,使⽤的回调嵌套的形式⼤多是异步函数,所以⼀些开发者可能会下意识的认为,凡是回调形式的函数都是异步流程。其实并不是这样的,真正的解释是(敲⿊板):JavaScript中的回调函数结构,默认是同步的结构,由于JavaScript单线程异步模型的规则,如果想要编写异步的代码,必须使⽤回调嵌套的形式才能实现,所以回调函数结构不⼀定是异步代码,但是异步代码⼀定是回调函数结构。 },1000) })}).then(function(){ //第三秒后执⾏的逻辑 console.log('第三秒之后发⽣的事情')})//fn:是初始化过程中调⽤的函数他是同步的回调函数var p = new Promise(fn)//把fn当作函数对象那么就可以在test函数中使⽤()执⾏他function test(fn){ fn()}//那么运⾏test的时候fn也会随着执⾏,所以test中传⼊的匿名函数就会运⾏test(function(){ ...})那么为什么异步流程都需要回调函数?依然举个栗⼦:分析下列代码的输出顺序(先不要看答案):分析下列代码的输出顺序(先不要看答案):再思考⼀下,如果我们有⼀个变量a的值为0,想要1秒之后设置他的值为1,并且我们想要在之后得到a的新结果,这个逻辑中如果1秒之后设置a为1采⽤的是setTimeout,那么我们在同步结构⾥能否实现?先参考下⾯的案例:解析上述代码块输出的结果⼀定是0,由于JavaScript单线程异步模型的知识,我们可以得知,当前的代码块中setTimeout的回调函数是⼀个宏任务,会在本次的同步代码执⾏完毕后执⾏,所以声明a=0和输出a的值这两⾏代码会优先执⾏,这时对a设置1的事件还没有发⽣,所以输出的结果就⼀定为0。接下来对代码做如下改造,我们试图使⽤阻塞的⽅式来获取异步代码的结果:function test(fn){ fn()}console.log(1)test(function(){ console.log(2)})console.log(3)//这段代码的输出顺序应该是*、*、*,因为他属于直接进⼊执⾏栈的程序,会按照正常程序解析的流程输出function test(fn){ setTimeout(fn,0)}console.log(1)test(function(){ console.log(2)})console.log(3)//这段代码会输出*、*、*,因为在调⽤test的时候settimeout将fn放到了异步任务队列挂起了,等待主程序执⾏完毕之后才会执⾏var a = 0setTimeout(function(){ a = 1},1000)console.log(a)解析本案例的同步代码会在while循环中阻塞2秒,所以console.log(a)这⾏代码会在2秒之后才能获得执⾏资源,但是最终输出的结果仍然是0。这是为什么呢?这⾥仍然可以通过JavaScript的运⾏模型来进⾏理解,由于单线程异步模型的规则是严格的同步在前异步靠后顺序,本案例的同步代码虽然阻塞了2秒,已经超过了setTimeout的等待时间,但是setTimeout中的宏任务到时间后,仅仅会被从⼯作线程移动到任务队列中进⾏等待。在时间到达1秒时,while循环没有执⾏结束,所以函数执⾏栈会被继续占⽤,直到循环释放并输出a之后,任务队列中的宏任务才能执⾏,所以这⾥就算setTimeout时间到了,也必须等待同步代码执⾏完毕,那么输出a的时候a=1的事件仍然没有发⽣,所以我们采⽤默认的上下结构永远拿不到异步回调中的结果,这也是为什么异步流程都是回调函数的原因。所以想要真正的在2秒后获取a的新结果的代码结构是这样的:到这⾥我们⼤概明⽩了回调函数的意义以及使⽤场景了,那么我们的Promise对象完整的结构是接下来案例中的样⼦,并且他是⼀个及特殊的存在,Promise中既包含同步的回调函数,⼜包含异步的回调函数。Promise案例介绍运⾏该案例并查看解析:var a = 0//依然使⽤setTimeout设置1秒的延迟设置a的值setTimeout(function(){ a = 1},1000)var d = new Date().getTime()var d1 = new Date().getTime()//采⽤while循环配合时间差来阻塞同步代码2秒while(d1-d<2000){ d1 = new Date().getTime()}console.log(a)//我们只有在这个回调函数中才能获取到a改造之后的结果var a = 0setTimeout(function(){ a = 1},1000)//注册⼀个新的宏任务,让他在上⼀个宏任务后执⾏setTimeout(function(){ console.log(a)},2000)参考上⾯的Promise对象结构,⼀个Promise对象包含两部分回调函数,第⼀部分是new Promise时候传⼊的对象,这段回调函数是同步的,⽽.then.catch.finally中的回调函数是异步的,这⾥我们提前记好。接下来可以在html⻚⾯中跑⼀遍程序,会发现这段程序并没有任何输出,然后我们可以将程序继续改造。练习案例:猜输出顺序将这段程序运⾏⼀下会发现输出顺序为:起步->调⽤resolve->结束->执⾏了resolve->then执⾏->finally执⾏再看下⾯的代码://实例化⼀个Promise对象var p = new Promise(function(resolve,reject){ })//通过链式调⽤控制流程p.then(function(){ console.log('then执⾏')}).catch(function(){ console.log('catch执⾏')}).finally(function(){ console.log('finally执⾏')})console.log('起步')var p = new Promise(function(resolve,reject){ console.log('调⽤resolve') resolve('执⾏了resolve')})p.then(function(res){ console.log(res) console.log('then执⾏')}).catch(function(){ console.log('catch执⾏')}).finally(function(){ console.log('finally执⾏')})console.log('结束')console.log('起步')var p = new Promise(function(resolve,reject){ console.log('调⽤reject') reject('执⾏了reject')})p.then(function(res){ console.log(res) console.log('then执⾏')}).catch(function(res){将这段程序运⾏⼀下会发现输出顺序为:起步->调⽤reject->结束->执⾏了reject->catch执⾏->finally执⾏解读Promise结构经过了上⾯的代码我们可以分析⼀下Promise的运⾏流程和结构,⾸先从运⾏流程上我们发现了new Promise中的回调函数确实是在同步任务中执⾏的,其次是如果这个回调函数内部没有执⾏resolve或者reject那么p对象的后⾯的回调函数内部都不会有输出,⽽运⾏resolve函数之后.then和.finally就会执⾏,运⾏了reject之后.catch和.finally就会执⾏。剖析对象结构Pomise对象相当于⼀个未知状态的对象,他的定义就是声明⼀个等待未来结果的对象,在结果发⽣之前他⼀直是初始状态,在结果发⽣之后他会变成其中⼀种⽬标状态,它的名字Promise中⽂翻译为保证。很多国外的电影台词都会涉及到Promsie这个单词,⽐如⼩明发现了邻居张三的妻⼦出轨了,在某天喝酒的时候⼩明和张三说:I sawyour wife played with other man! I promise ! I saw it!张三当然会说:No !shit!I can not trust you!dame!(语义⾃⾏理解),Promise在英⽂中是绝对保证的意思,所以在编程中Promise对象是⼀个⾮常严谨的对象,⼀定会按照约定执⾏,不会出现任务灵异问题(除使⽤不当外)。那么Promise本身具备三种状态:pending:初始状态,也叫就绪状态,这是在Promise对象定义初期的状态,这时Promise仅仅做了初始化并注册了他对象上所有的任务。fulfilled:已完成,通常代表成功执⾏了某⼀个任务,当初始化函数中的resolve执⾏时,Promise的状态就变更为fulfilled,并且then函数注册的回调函数会开始执⾏,resolve中传递的参数会进⼊回调函数作为形参。rejected:已拒绝,通常代表执⾏了⼀次失败任务,或者流程中断,当调⽤reject函数时,catch注册的回调函数就会触发,并且reject中传递的内容会变成回调函数的形参。三种状态之间的关系:Promise中约定,当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏。根据上⾯的分析,结合下⾯的代码案例学习Promise的规则,分析该对象的运⾏结果: console.log(res) console.log('catch执⾏')}).finally(function(){ console.log('finally执⾏')})console.log('结束')通过分析以上的说明我们知道了Promise对象存在三种状态以及他们之间的关系,那么我们在执⾏本段程序的时候会发现这个段代码的输出结果是:then执⾏->finally执⾏接下来查看另⼀个案例:我们在执⾏本段程序的时候会发现这个段代码的输出结果是:catch执⾏->finally执⾏再接下来查看下⾯的案例:我们在执⾏本段程序的时候会发现这个段代码的输出结果是:空new Promise(function(resolve,reject){ resolve() reject()}).then(function(){ console.log('then执⾏')}).catch(function(){ console.log('catch执⾏')}).finally(function(){ console.log('finally执⾏')})new Promise(function(resolve,reject){ reject() resolve()}).then(function(){ console.log('then执⾏')}).catch(function(){ console.log('catch执⾏')}).finally(function(){ console.log('finally执⾏')})new Promise(function(resolve,reject){}).then(function(){ console.log('then执⾏')}).catch(function(){ console.log('catch执⾏')}).finally(function(){ console.log('finally执⾏')})总结分析了对象结构和状态后,我们了解了Promise的异步回调部分如何执⾏,取决于我们在初始化函数中的操作,并且初始化函数中⼀旦调⽤了resolve后⾯再执⾏reject也不会影响then执⾏,catch也不会执⾏,反之同理。⽽在初始化回调函数中,如果不执⾏任何操作,那么promise的状态就仍然是pending,所有注册的回调函数都不会执⾏。

关于链式调⽤


链式调⽤这个⽅式最经典的体现是在JQuery框架上,到现在仍然很多语⾔都在使⽤这种优雅的语法(不限前端还是后台),所以我们来简单认识⼀下什么是链式调⽤,为什么Promise对象可以.then().catch()这样调⽤。为什么还能.then().then()这样调⽤,他的原理⼤概是这样的。其实他的本质就是在我们调⽤这些⽀持链式调⽤的函数的结尾时,他⼜返回了⼀个包含他⾃⼰的对象或者是⼀个新的⾃⼰,这些⽅式都可以实现链式调⽤。Promise使⽤注意事项在⽹⻚中运⾏如下代码查看返回结果:控制台上会得到如下内容:[[Prototype]]代表Promise的原型对象[[PromiseState]]代表Promise对象当前的状态[[PromiseResult]]代表Promise对象的值,分别对应resolve或reject传⼊的结果function MyPromise(){ return this}MyPromise.prototype.then = function(){ console.log('触发了then') return this}new MyPromise().then().then().then()var p = new Promise(function(resolve,reject){ resolve('我是Promise的值')})console.log(p)Promise {: '我是Promise的值'}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: "我是Promise的值"1. 链式调⽤的注意事项运⾏如下代码并查看结果:控制台上会输出如下结果:根据现象我们可以分析出链式调⽤的基本形式(极其重要):1. 只要有then()并且触发了resolve,整个链条就会执⾏到结尾,这个过程中的第⼀个回调函数的参数是resolve传⼊的值2. 后续每个函数都可以使⽤return返回⼀个结果,如果没有返回结果的话下⼀个then中回调函数的参数就是undefined3. 返回结果如果是普通变量,那么这个值就是下⼀个then中回调函数的参数//通过⼀个超⻓的链式调⽤我们学习⼀下链式调⽤的注意事项var p = new Promise(function(resolve,reject){ resolve('我是Promise的值')})console.log(p)p.then(function(res){ //该res的结果是resolve传递的参数 console.log(res)}).then(function(res){ //该res的结果是undefined console.log(res) return '123'}).then(function(res){ //该res的结果是123 console.log(res) return new Promise(function(resolve){ resolve(456) })}).then(function(res){ //该res的结果是456 console.log(res) return '我是直接返回的结果'}).then() .then('我是字符串') .then(function(res){ //该res的结果是“我是直接返回的结果” console.log(res)})Promise {: '我是Promise的值'}ttt.html:16 我是Promise的值ttt.html:18 undefinedttt.html:21 123ttt.html:26 456ttt.html:31 我是直接返回的结果4. 如果返回的是⼀个Promise对象,那么这个Promise对象resolve的结果会变成下⼀次then中回调的函数的参数5. 如果then中传⼊的不是函数或者未传值,Promise链条并不会中断then的链式调⽤,并且在这之前最后⼀次的返回结果,会直接进⼊离它最近的正确的then中的回调函数作为参数2. 中断链式调⽤链式调⽤可以中断吗?答案是肯定的,我们有两种形式可以让.then的链条中断,如果中断还会触发⼀次.catch的执⾏。查阅下⾯的案例学习:结果如下我们发现中断链式调⽤后会触发catch函数执⾏,并且从中断开始到catch中间的then都不会执⾏,这样链式调⽤的流程就结束了,中断的⽅式可以使⽤抛出⼀个异常或返回⼀个rejected状态的Promise对象。3. 中断链式调⽤是否违背了Promise的精神?我们在介绍Promise的时候强调了他是绝对保证的意思,并且Promise对象的状态⼀旦变更就不会再发⽣变化。当我们使⽤链式调⽤的时候正常都是then函数链式调⽤,但是当我们触发中断的时候catch却执⾏了。按照约定规则then函数执⾏,就代表Promise对象的状态已经变更为fulfilled了,但是catch函数执⾏时,Promise对象应该是rejected状态啊!这不科学。在得到科学的解释前,先下⾯举个例⼦:var p = new Promise(function(resolve,reject){ resolve('我是Promise的值')})console.log(p)p.then(function(res){ console.log(res)}).then(function(res){ //有两种⽅式中断Promise // throw('我是中断的原因') return Promise.reject('我是中断的原因')}).then(function(res){ console.log(res)}).then(function(res){ console.log(res)}).catch(function(err){ console.log(err)})Promise {: '我是Promise的值'}ttt.html:16 我是Promise的值ttt.html:26 我是中断的原因当我们运⾏上⾯的代码时,控制台会出现如下的打印信息:我们会发现返回的p和p1 的状态本身就不⼀样,并且他们的对⽐结果是false,这就代表他们在堆内存中开辟了两个空间,p和p1对象分别保存了两个Promise对象的引⽤地址,所以then函数虽然每次都返回Promise对象,来实现链式调⽤,但是then函数每次返回的都是⼀个新的Promise对象。这样便解释的通了!也就是说每⼀次then函数在执⾏时,我们都可以让本次的结果在下⼀个异步步骤执⾏时,变成不同的状态,⽽且这也不违背Promise对象最初的约定。4. 总结根据以上的分析我们已经掌握了Promise在运⾏时的规则,这样就能解释的通,为什么最初通过Promise控制setTimeout每秒执⾏⼀次的功能可以实现,这是因为当我们使⽤then函数进⾏链式调⽤时,可以利⽤返回⼀个新的Promise对象来执⾏下⼀次then函数,⽽下⼀次then函数的执⾏,必须等待其内部的resolve调⽤。这样我们在new Promise时,放⼊setTimeout来进⾏延时,保证1秒之后让状态变更,这样就能不编写回调嵌套来实现连续的执⾏异步流程了。




Promise常⽤api




Promise.all()当我们在代码中需要使⽤异步流程控制时,可以通过Promise.then来实现让异步流程⼀个接⼀个的执⾏,假设实际案例中,某个模块的⻚⾯需要同时调⽤3个服务端接⼝,并保证三个接⼝的数据全部返回后,才能渲染⻚⾯。这种情况如果a接⼝耗时1s、b接⼝耗时0.8s、c接⼝耗时1.4s,如果只⽤romise.then来执⾏流程控制,可以保证三个接⼝按顺序调⽤结束再渲染⻚⾯,但是如果通过then函数的异步控制,必须等待每个接⼝调⽤完毕才能调⽤下⼀个,这样总耗时就是1+0.8+1.4 = 3.2s。这种累加显然增加了接⼝调⽤的时间消耗,所以Promise提供了⼀个all⽅法来解决这个问题:Promise.all([promise对象,promise对象,...]).then(回调函数)回调函数的参数是⼀个数组,按照第⼀个参数的promise对象的顺序展示每个promise的返回结果。我们可以借助Promise.all来实现,等最慢的接⼝返回数据后,⼀起得到所有接⼝的数据,那么这个耗时将会只会按照最慢接⼝的消耗时间1.4s执⾏,总共节省了1.8s,参考代码如下:var p = new Promise(function(resolve,reject){ resolve('我是Promise的值')})var p1 = p.then(function(res){})console.log(p)console.log(p1)console.log(p1===p)Promise {: '我是Promise的值'}ttt.html:18 Promise {}ttt.html:19 false//promise.all相当于统⼀处理了Promise.race()race⽅法与all⽅法使⽤格式相同:Promise.race([promise对象,promise对象,...]).then(回调函数)回调函数的参数是前⾯数组中最快⼀个执⾏完毕的promise的返回值。所以使⽤race⽅法主要的使⽤场景是什么样的呢?举个例⼦,假设我们的⽹站有⼀个播放视频的⻚⾯,通常流媒体播放为了保证⽤户可以获得较低的延迟,都会提供多个媒体数据源。我们希望⽤户在进⼊⽹⻚时,优先展示的是这些数据源中针对当前⽤户速度最快的那⼀个,这时便可以使⽤romise.race()来让多个数据源进⾏竞赛,得到竞赛结果后,将延迟最低的数据源⽤于⽤户播放视频的默认数据源,这个场景便是race的⼀个典型使⽤场景。下⾯我们可以参数考代码案例来查看race的介绍://多个promise任务,保证处理的这些所有promise//对象的状态全部变成为fulfilled之后才会出发all的//.then函数来保证将放置在all中的所有任务的结果返回let p1 = new Promise((resolve,reject) => { setTimeout(() => { resolve('第⼀个promise执⾏完毕') },1000)})let p2 = new Promise((resolve,reject) => { setTimeout(() => { resolve('第⼆个promise执⾏完毕') },2000)})let p3 = new Promise((resolve,reject) => { setTimeout(() => { resolve('第三个promise执⾏完毕') },3000)})Promise.all([p1,p3,p2]).then(res => { console.log(res)}).catch(function(err){ console.log(err)})//promise.race()相当于将传⼊的所有任务//进⾏了⼀个竞争,他们之间最先将状态变成fulfilled的//那⼀个任务就会直接的触发race的.then函数并且将他的值//返回,主要⽤于多个任务之间竞争时使⽤let p1 = new Promise((resolve,reject) => { setTimeout(() => { resolve('第⼀个promise执⾏完毕') },5000)})let p2 = new Promise((resolve,reject) => {Promise的演进在介绍了这么多Promise对象后,我们发现他的能⼒⼗分强⼤,使⽤模式⾮常的⾃由,并且将JavaScript⼀个时代的弊病从此“解套”。这个解套虽然⽐较成功,但是如果直接使⽤then()函数进⾏链式调⽤,我们的代码量仍然是⾮常沉重的,想要开发⼀个⾮常复杂的异步流程,依然需要⼤量的链式调⽤进⾏⽀撑,开发者还是会变得⾮常的难受。按照⼈类的线性思维,虽然JavaScript分同步和异步,但是单线程模式下,如果能完全按照同步代码的编写⽅式来处理异步流程,这才是最奈斯的结果,那么有没有办法让Promise对象能更进⼀步的接近同步代码呢?


Generator函数的介绍




在JavaScript中存在这样⼀种函数,我们先看⼀下这个函数的样⼦ES6 新引⼊了 Generator 函数,可以通过 yield 关键字,把函数的执⾏流挂起,为改变执⾏流程提供了可能,从⽽为异步编程提供解决⽅案。 所以他的存在提供了让函数可以进⾏分步执⾏的能⼒。举个栗⼦: setTimeout(() => { reject('第⼆个promise执⾏完毕') },2000)})let p3 = new Promise(resolve => { setTimeout(() => { resolve('第三个promise执⾏完毕') },3000)})Promise.race([p1,p3,p2]).then(res => { console.log(res)}).catch(function(err){ console.error(err)})function * fnName(){ yield *** yield ***}//该函数和普通函数不同,在执⾏的时候函数并不会运⾏并且会返回⼀个分步执⾏对象//该对象存在next⽅法⽤来让程序继续执⾏,当程序遇到yield关键字的时候会停顿//next返回的对象中包含value和done两个属性,value代表上⼀个yield返回的结果//done代表程序是否执⾏完毕function * test(){ var a = yield 1 console.log(a) var b = yield 2 console.log(b) var c = a+b console.log(c)我们查看程序的注释并且运⾏该程序看控制台的结果:查看结果我们发现a和b的值不⻅了,c也是NaN虽然程序实现了分步执⾏,但是流程却出现了问题。这是因为在分步执⾏过程中,我们是可以在程序中对运⾏的结果进⾏⼈为⼲预的,也就是说yield返回的结果和他左侧变量的值都是我们可以⼲预的。接下来我们改造代码如下:}//获取分步执⾏对象var generator = test()//输出console.log(generator)//步骤1 该程序从起点执⾏到第⼀个yield关键字后,step1的value是yield右侧的结果1var step1 = generator.next()console.log(step1)//步骤2 该程序从var a开始执⾏到第2个yield后,step2的value是yield右侧的结果2var step2 = generator.next()console.log(step2)//由于没有yield该程序从var b开始执⾏到结束var step3 = generator.next()console.log(step3)test {}[[GeneratorLocation]]: ttt.html:10[[Prototype]]:Generator[[GeneratorState]]: "closed"[[GeneratorFunction]]: ƒ * test()[[GeneratorReceiver]]: Windowttt.html:21 {value: 1, done: false}ttt.html:12 undefinedttt.html:23 {value: 2, done: false}ttt.html:14 undefinedttt.html:16 NaNttt.html:25 {value: undefined, done: true}function * test(){ var a = yield 1 console.log(a) var b = yield 2 console.log(b) var c = a+b console.log(c)}var generator = test()console.log(generator)var step1 = generator.next()console.log(step1)var step2 = generator.next(step1.value)console.log(step2)var step3 = generator.next(step2.value)当我们将代码改造成上⾯的结构之后我们发现控制台中的数据就正确了:也就是说next函数执⾏的过程中我们是需要传递参数的,当下⼀次next执⾏的时候我们如果不传递参数,那么本次yield左侧变量的值就变成了undefined,所以我们如果想让yield左侧的变量有值就必须在next中传⼊指定的结果。Generator能控制什么样的流程?⾸先查看下列代码然后查看他的输出结果:console.log(step3)test {}ttt.html:21 {value: 1, done: false}ttt.html:12 1ttt.html:23 {value: 2, done: false}ttt.html:14 2ttt.html:16 3ttt.html:25 {value: undefined, done: true}function * test(){ var a = yield 1 console.log(a) var res = yield setTimeout(function(){ return 123 },1000) console.log(res) var res1 = yield new Promise(function(resolve){ setTimeout(function(){ resolve(456) },1000) }) console.log(res1)}var generator = test()console.log(step)var step1 = generator.next()console.log(step1)var step2 = generator.next()console.log(step2)var step3 = generator.next()console.log(step3)var step4 = generator.next()console.log(step4)根据调⽤情况我们可以⾃⾏测试,会发现输出结果时并没有任何的延迟,并且我们观察打印输出会发现普通变量可以直接在value中拿到,setTimeout位置我们拿到的值和回调函数内部的值完全不⼀样,⽽romise对象我们可以拿到它本身。接下来我们展开查看Promise对象:我们发现Promise对象中是可以获取到内部的结果的,那么我们在Generator函数中能确保的就是,在分步过程中,能中使⽤romise和普通对象都能拿到运⾏流程的结果,但是JavaScript中的setTimeout我们还是⽆法直接控制它的流程。实现⽤Generator将Promise的异步流程同步化通过上⾯的观察,我们可以通过递归调⽤的⽅式,来动态的去执⾏⼀个Generator函数,以done属性作为是否结束的依据,通过next来推动函数执⾏,如果过程中遇到了Promise对象我们就等待Promise对象执⾏完毕再进⼊下⼀步,我们这⾥排除异常和对象reject的情况,封装⼀个动态执⾏的函数如下:test {}ttt.html:27 {value: 1, done: false}ttt.html:12 undefinedttt.html:29 {value: 1, done: false}ttt.html:16 undefinedttt.html:31 {value: Promise, done: false}ttt.html:22 undefinedttt.html:33 {value: undefined, done: true}{value: Promise, done: false}done: falsevalue: Promise[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: 456[[Prototype]]: Object/*** fn:Generator函数对象*/function generatorFunctionRunner(fn){ //定义分步对象 let generator = fn() //执⾏到第⼀个yield let step = generator.next() //定义递归函数 function loop(stepArg,generator){ //获取本次的yield右侧的结果 let value = stepArg.value //判断结果是不是Promise对象 if(value instanceof Promise){ //如果是Promise对象就在then函数的回调中获取本次程序结果 //并且等待回调执⾏的时候进⼊下⼀次递归 value.then(function(promiseValue){有了这个函数之后我们就可以将最初的三个setTimeout转换成如下结构进⾏开发当我们通过上⾯的运⾏⼯具函数之后我们就可以在控制台看⻅每间隔1秒钟就输出⼀次 if(stepArg.done == false){ loop(generator.next(promiseValue),generator) } }) }else{ //判断程序没有执⾏完就将本次的结果传⼊下⼀步进⼊下⼀次递归 if(stepArg.done == false){ loop(generator.next(stepArg.value),generator) } } } //执⾏动态调⽤ loop(step,generator)}function * test(){ var res1 = yield new Promise(function(resolve){ setTimeout(function(){ resolve('第⼀秒运⾏') },1000) }) console.log(res1) var res2 = yield new Promise(function(resolve){ setTimeout(function(){ resolve('第⼆秒运⾏') },1000) }) console.log(res2) var res3 = yield new Promise(function(resolve){ setTimeout(function(){ resolve('第三秒运⾏') },1000) }) console.log(res3)}generatorFunctionRunner(test)第⼀秒运⾏ttt.html:22 第⼆秒运⾏ttt.html:28 第三秒运⾏经过这个yield修饰符之后我们惊喜的发现,抛去generatorFunctionRunner函数外,我们在Generator函数中已经可以将Promise的.then回调成功的规避了,yield修饰的Promise对象在运⾏到当前⾏时,程序就会进⼊挂起状态直到Promise对象变成完成状态,程序才会向下⼀⾏执⾏。这样我们就通过Generator函数对象成功的将Promise对象同步化了。这也是JavaScript异步编程的⼀个过渡期,通过这个解决⽅案,只需要提前准备好⼯具函数那么编写异步流程可以很轻松的使⽤yield关键字实现同步化。


关于Async和Await




经过了Generator的过渡之后异步代码同步化的需求逐渐成为了主流需求,这个过程在ES7版本中得到了提案,并在ES8版本中进⾏了实现,提案中定义了全新的异步控制流程。查看代码结构之后我们发现他的编写⽅式与Generator函数结构很相似,提案中规定了我们可以使⽤async修饰⼀个函数,这样就能在该函数的直接⼦作⽤域中,使⽤await来⾃动的控制函数的流程,await 右侧可以编写任何变量或对象,当右侧是普通对象的时候函数会⾃动返回右侧的结果并向下执⾏,⽽当await右侧为Promise对象时,如果Promise对象状态没有变成完成,函数就会挂起等待,直到Promise对象变成fulfilled,程序再向下执⾏,并且Promise的值会⾃动返回给await左侧的变量中。async和await需要成对出现,async可以单独修饰函数,但是await只能在被async修饰的函数中使⽤。有了await和async就相当于使⽤了⾃带执⾏函数的Generator函数,这样我们就不再需要单独针对Generator函数进⾏开发了,所以async和await逐渐成为主流异步流程控制的终极解决⽅案。⽽Generator慢慢淡出了业务开发者的舞台,不过Generator函数成为了向下兼容过渡期版本浏览器的候补实现⽅式,虽然在现今的⼤部分项⽬业务中使⽤Generator函数的场景⾮常的少,但是如果查看脚⼿架项⽬中通过babel构建的JavaScript⽣产代码,我们还是能⼤量的发现Generator的应⽤的,他的作⽤就是为了兼容不⽀持async和await的浏览器。认识async函数创建如下函数,执⾏并查看控制台输出:输出控制台如下://提案中定义的函数使⽤成对的修饰符async function test(){ await ... await ...}test()async function test(){ return 1}let res = test()console.log(res)Promise {: 1}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: 1根据控制台结果我们发现其实async修饰的函数,本身就是⼀个Promise对象,虽然我们在函数中return的值是1,是使⽤了async修饰之后,这个函数运⾏时并没有直接返回1,⽽是返回了⼀个值为1的Promise对象。接下来我们测试如下流程,先分析运⾏结果,猜测输出的顺序:执⾏该流程之后发现输出的结果是1,3,2。很惊喜是不是!按照Promise对象的执⾏流程function被async修饰之后它本身应该变成异步函数,那么他应该在1和2输出完毕之后在输出3,但是结果却出⼈意料,这⼜⼀次打破了单线程异步模型的概念。别急,冷静⼀下,先回想⼀下Promise对象的结构:我们在介绍Promise对象时,特别介绍了⼀下回调函数,并且强调他是⼀个极少数的既使⽤同步回调流程⼜使⽤了异步的回调流程的对象,所以在new Promise时的function是同步流程。现在介绍这个和刚才的输出有关系吗?当然有,接下来查看下⾯的逻辑,还是先猜测⼀下输出顺序:我们发现奇怪的事情⼜发⽣了,控制台输出的顺序是1,3,2,4按照我们⼀开始以为的流程,test函数应该是同步逻辑,那么3和4应该是连着输出的,他不应该会出现3在2之前4在2之后输出的情况,这个同步逻辑和异步逻辑都说不过去,那么我们将当前的函数翻译⼀下,由于async修饰的函数会被解释成Promise对象,所以我们可以将其翻译成如下结构:async function test(){ console.log(3) return 1}console.log(1)test()console.log(2)new Promise(function(){}).then(function(){})async function test(){ console.log(3) var a = await 4 console.log(a) return 1}console.log(1)test()console.log(2)看到这个Promise对象我们就豁然开朗,由于初始化的回调是同步的所以1,3,2都是同步代码⽽4是在resolve中传⼊的,then代表异步回调所以4应该最后输出。综上所述,async函数中有⼀个最⼤的特点,就是第⼀个await会作为分⽔岭⼀般的存在,在第⼀个await的右侧和上⾯的代码,全部是同步代码区域相当于new Promise的回调,第⼀个await的左侧和下⾯的代码,就变成了异步代码区域相当于then的回调,所以就出现上⾯我们发现的灵异问题。最终的setTimeout解决代码经过了两个时代的变⾰,现在我们可以使⽤如下的⽅式来进⾏流程控制,不再需要依赖⾃⼰定义的流程控制器函数来进⾏分步执⾏,这⼀切的核⼼起源都是Promise对象的规则定义开始的,所以最终我们的解决⽅案如下。总结console.log(1)new Promise(function(resolve){ console.log(3) resolve(4)}).then(function(a){ console.log(a)})console.log(2)async function test(){ var res1 = await new Promise(function(resolve){ setTimeout(function(){ resolve('第⼀秒运⾏') },1000) }) console.log(res1) var res2 = await new Promise(function(resolve){ setTimeout(function(){ resolve('第⼆秒运⾏') },1000) }) console.log(res2) var res3 = await new Promise(function(resolve){ setTimeout(function(){ resolve('第三秒运⾏') },1000) }) console.log(res3)}test()从回调地狱到Promise的链式调⽤到Generator函数的分步执⾏再到async和await的⾃动异步代码同步化机制,经历了很多个年头,所以⾯试中为什么经常问到Promise,并且重点沿着Promise对象深⼊的挖掘去问你各种问题,主要是考察程序员对Promise对象本身以及他的发展历程是否有深⼊的了解,同时也是在考察⾯试者对JavaScript的事件循环系统和异步编程的基本功是否⾜够的扎实。Promise和事件循环系统并不是JavaScript中的⾼级知识,⽽是真正的基础知识,所以所有⼈想要在⾏业中更好的发展下去,这些知识都是必备基础,必须扎实掌握。我们未来对⾃⼰的定位是软件开发/研发⼯程师,并不是码农

QQ|Archiver|手机版|小黑屋|firemail ( 粤ICP备15085507号-1 )

GMT+8, 2024-5-12 06:24 , Processed in 0.069103 second(s), 23 queries .

Powered by Discuz! X3

© 2001-2013 Comsenz Inc.

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