romise.then catch finally✅在JavaScript的运⾏环境中,代码的执⾏流程是这样的:1. 默认的同步代码按照顺序从上到下,从左到右运⾏,运⾏过程中注册本次的微任务和后续的宏任务:2. 执⾏本次同步代码中注册的微任务,并向任务队列注册微任务中包含的宏任务和微任务3. 将下⼀个宏任务开始前的所有微任务执⾏完毕4. 执⾏最先进⼊队列的宏任务,并注册当次的微任务和后续的宏任务,宏任务会按照当前任务队列的队尾继续向下排列常⻅的宏任务和微任务划分宏任务有些地⽅会列出来 UI Rendering ,说这个也是宏任务,可是在读了HTML规范⽂档以后,发现这很显然是和微任务平⾏的⼀个操作步骤requestAnimationFrame 姑且也算是宏任务吧, requestAnimationFrame 在MDN的定义为,下次⻚⾯重绘前所执⾏的操作,⽽重绘也是作为宏任务的⼀个步骤来存在的,且该步骤晚于微任务的执⾏微任务经典笔试题代码输出顺序问题1setTimeout(function() {console.log('timer1')}, 0)requestAnimationFrame(function(){ console.log('UI update')解析:本案例输出的结果为:猜对我就告诉你,先思考,猜对之后结合运⾏结果分析。按照同步先⾏,异步靠后的原则,阅读代码时,先分析同步代码和异步代码,Promise对象虽然是微任务,但是new Promise时的回调函数是同步执⾏的,所以优先输出promise 1 和 promise 2。在resolve执⾏时Promise对象的状态变更为已完成,所以then函数的回调被注册到微任务事件中,此时并不执⾏,所以接下来应该输出end。同步代码执⾏结束后,观察异步代码的宏任务和微任务,在本次的同步代码块中注册的微任务会优先执⾏,参考上⽂中描述的列表,Promise为微任务,setTimeout和requestAnimationFrame为宏任务,所以Promise的异步任务会在下⼀个宏任务执⾏前执⾏,所以promise then是第四个输出的结果。接下来参考setTimeout和requestAnimationFrame两个宏任务,这⾥的运⾏结果是多种情况。如果三个宏任务都为setTimeout的话会按照代码编写的顺序执⾏宏任务,⽽中间包含了⼀个requestAnimationFrame,这⾥就要学习⼀下他们的执⾏时机了。setTimeout是在程序运⾏到setTimeout时⽴即注册⼀个宏任务,所以两个setTimeout的顺序⼀定是固定的timer1和timer2会按照顺序输出。⽽requestAnimationFrame是请求下⼀次重绘事件,所以他的执⾏频率要参考浏览器的刷新率。参考如下代码:})setTimeout(function() {console.log('timer2')}, 0)new Promise(function executor(resolve) { console.log('promise 1') resolve() console.log('promise 2')}).then(function() { console.log('promise then')})console.log('end')let i = 0;let d = new Date().getTime()let d1 = new Date().getTime()function loop(){ d1 = new Date().getTime() i++ //当间隔时间超过1秒时执⾏ if((d1-d)>=1000){ d = d1 console.log(i) i = 0 console.log('经过了1秒') } requestAnimationFrame(loop)}该代码在浏览器运⾏时,控制台会每间隔1秒进⾏⼀次输出,输出的i就是loop函数执⾏的次数,如下图:这个输出意味着requestAnimationFrame函数的执⾏频率是每秒钟60次左右,他是按照浏览器的刷新率来进⾏执⾏的,也就是当屏幕刷新⼀次时该函数就会触发⼀次,相当于运⾏间隔是16毫秒左右。继续参考下列代码:该代码结构与上⾯的案例类似,循环是采⽤setTimeout进⾏控制的,所以参考运⾏结果,如图:loop()let i = 0;let d = new Date().getTime()let d1 = new Date().getTime()function loop(){ d1 = new Date().getTime() i++ if((d1-d)>=1000){ d = d1 console.log(i) i = 0 console.log('经过了1秒') } setTimeout(loop,0)}loop()根据运⾏结果得知,setTimeout(fn,0)的执⾏频率是每秒执⾏200次左右,所以他的间隔是5毫秒左右。由于这两个异步的宏任务出发时机和执⾏频率不同,会导致三个宏任务的触发结果不同,如果我们打开⽹⻚时,恰好赶上5毫秒内执⾏了⽹⻚的重绘事件,requestAnimationFrame在⼯作线程中就会到达触发时机优先进⼊任务队列,所以此时会输出:UI update->timer1->timer2。⽽当打开⽹⻚时上⼀次的重绘刚结束,下⼀次重绘的触发是16毫秒后,此时setTimeout注册的两个任务在⼯作线程中就会优先到达触发时机,这时输出的结果是:timer1->timer2->UI update。所以此案例的运⾏结果如下2图所示:代码输出顺序问题2解析:仍然是猜对了告诉你哈~,先运⾏⼀下试试吧。这个案例代码简单易懂,但是很容易引起错误答案的出现。由于该事件是直接绑定在document上的,所以点击⽹⻚就会触发该事件,在代码运⾏时相当于按照顺序注册了两个点击事件,两个点击事件会被放在⼯作线程中实时监听触发时机,当元素被点击时,两个事件会按照先后的注册顺序放⼊异步任务队列中进⾏执⾏,所以事件1和事件2会按照代码编写的顺序触发。这⾥就会导致有⼈分析出错误答案:2,4,1,3。为什么不是2,4,1,3呢?由于事件执⾏时并不会阻断JS默认代码的运⾏,所以事件任务也是异步任务,并且是宏任务,所以两个事件相当于按顺序执⾏的两个宏任务。这样就会分出两个运⾏环境,第⼀个事件执⾏时,console.log(2);是第⼀个宏任务中的同步代码,所以他会⽴即执⾏,⽽
romise.resolve().then(()=> console.log(1));属于微任务,他会在下⼀个宏任务触发前执⾏,所以这⾥输出2后会直接输出1.⽽下⼀个事件的内容是相同道理,所以输出顺序为:2,1,4,3。总结document.addEventListener('click', function(){ Promise.resolve().then(()=> console.log(1)); console.log(2);})document.addEventListener('click', function(){ Promise.resolve().then(()=> console.log(3)); console.log(4);})关于事件循环模型今天就介绍到这⾥,在NodeJS中的事件循环模型和浏览器中是不⼀样的,本⽂是以浏览器的事件循环模型为基础进⾏介绍,事件循环系统在JavaScript异步编程中占据的⽐重是⾮常⼤的,在⼯作中可使⽤场景也是众多的,掌握了事件循环模型就相当于,异步编程的能⼒上升了⼀个新的⾼度。
romise对象
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倍。
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对象能更进⼀步的接近同步代码呢?
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关键字实现同步化。| 欢迎光临 firemail (http://firemail.wang:8088/) | Powered by Discuz! X3 |