js异步等待完成后再进⾏下⼀步操作_我理解的JavaScript异步
编程
引⾔
引⾔
最开始学习JS的时候就从知道了JS是单线程的,天⽣异步,适合IO密集型,不适合CPU密集型。但是,多数初学者从来没有认真思考过⾃
⼰程序中的异步到底是怎么出现的,以及为什么会出现,也没有探索过处理异步的其他⽅法,甚⾄于⼀直在⽤callback来解决异步问题。
为什么会出现异步
浏览器内核的多线程
⼀个浏览器⾄少三个常驻线程:JavaScript引擎线程,浏览器GUI渲染线程,浏览器事件触发线程。
JS引擎,是基于事件驱动单线程执⾏的,JS引擎⼀直等待着任务队列中任务的到来,然后加以处理,浏
览器⽆论什么时候都只有⼀个JS线程在运⾏JS程序。
GUI线程,当界⾯需要重绘或由于某种操作引发回流时,该线程就会执⾏。⽽因为JS可以操作DOM元素,进⽽会影响到GUI的渲染结
果,因此JS引擎线程与GUI渲染线程是互斥的,也就是说当JS引擎线程处于运⾏状态时,GUI渲染线程将处于冻结状态。
浏览器事件触发线程,当⼀个事件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理,这些事件可来⾃
JavaScript引擎当前执⾏的代码块,setTimeout, 也可以来⾃浏览器内核的其他线程如⿏标点击,AJAX异步请求等,但由于JS的单线程关系,所有这些事件都得排队等待JS引擎处理。
事件循环机制
任务队列,也就是事件队列,分为 宏任务(macro-task) 和 微任务(micro-task)
循环机制如下:
1. 先顺序从上向下执⾏当前全局上下⽂
await和async使用方法
2. 遇到异步事件就将其交给对应的浏览器模块
3. 浏览器的模块处理完之后,宏任务放⼊宏任务队列,微任务放⼊微任务队列
4. 当函数调⽤栈清空,开始执⾏任务队列,先执⾏微任务队列,执⾏完微任务队列再执⾏宏任务队列
5. 当执⾏任务队列时,可以认为重新开了⼀个空的宏任务队列和⼀个空的微任务队列,将新产⽣的异步任务最终放⼊新的任务队列,当
前任务队列执⾏完成后,当前宏队列和微队列就清除,然后再去执⾏新的微任务队列,执⾏新的宏任务队列,新开微队列,新开宏队
列,⼀直循环下去,直到任务队列全部为空。
异步的处理⽅法
callback是最简单的,但不是最好的
//callback的⼀般使⽤形式$.ajax({ url:'www.pidanChen', success(data){ console.log(data) }})//延时处理try{ setTimeout(function(){ console.log('timeout') },500)}
1. 如果某个业务,依赖于上层业务的数据,上层业务⼜依赖于更上⼀层的数据,还采⽤回调的⽅式来处理异步的话,就会出现回调地狱,也
就是顺序问题。
2. 最严重的还不是编辑器中出现的倒三⾓形的代码,是控制反转。例如,我们调⽤了⼀个第三⽅组件的⽀付API,进⾏购买⽀付,正常情况发
现⼀切运⾏良好,但是假如某⼀天,第三⽅组件出问题了,可能多次调⽤传⼊的回调,也可能传回错误的数据。说到底,这样的回调嵌套,
控制权在第三⽅,对于回调函数的调⽤⽅式、时间、次数、顺序,回调函数参数都是不可控的,因为⽆论如何,并不总能保证第三⽅是可信
任的。
解决信任问题--Promise
//创建第⼀个Promiselet p = new Promise(function(res,rej){ if(err){ rej() }else{ res() }})p.then()
实例化⼀个Promise对象组要⼀个函数作为参数,该函数接受两个参数:resolve函数和reject函数;当实例化Promise构造函数时,将⽴
即调⽤该函数,随后返回⼀个Promise对象。通常,实例化时,会初始⼀个异步任务,在异步任务完成或失败时,调⽤resolve或reject函
数来完成或拒绝返回的Promise对象。另外需要注意的是,若传⼊的函数执⾏抛出异常,那么这个Promsie将被拒绝。
解决控制反转的信任问题
Promise并没有取消控制反转,⽽是把反转出去的控制再反转⼀次,也就是反转了控制反转。
它与普通的回调的⽅式的区别在于,普通的⽅式,回调成功之后的操作直接写在了回调函数⾥⾯,⽽这些操作的调⽤由第三⽅控制。在Promise的⽅式中,回调只负责成功之后的通知,⽽回调成功之后的操作放在了then的回调⾥⾯,由Promise精确控制。
Promise有这些特征:只能决议⼀次,决议值只能有⼀个,决议之后⽆法改变。任何then中的回调也只会被调⽤⼀次。
解决调⽤过早
Promise就根本不必担⼼这种问题即使是⽴即完成的Promise,也⽆法被同步观察到,即使这个Promise已经决议了,提供给then的回调总会是在js事件队列在当前完成后,再被调⽤,即异步调⽤。
解决回调过晚或没有调⽤
Promise本⾝不会回调过晚,只要决议了,它就会按照规定运⾏。⾄于服务器或者⽹络的问题,并不是Promise能解决的,⼀般这种情况会使⽤Promise的竞态API Promise.race加⼀个超时的时间
function timeoutPromise(delay){ return new Promise(function(res,rej){ setTimeout(function(){ rej('timeout') }, delay) })}P romise.race([dosomeThing(), timeoutProm 解决回调次数太少或太多的问题
由于Promise只能被决议⼀次,且决议之后⽆法改变,所以,即便是多次回调,也不会影响结果,决议之后的调⽤都会被忽略。
let fs = require('fs');var p1 = new Promise(function (resolve, reject) { fs.readFile('a.txt', 'utf8', function (err, data) { if (err) { reject(err); } resolve(data); resolv
解决吞掉可能出现的错误或异常
如果在Promise的创建过程中或在查看其决议结果的过程中的任何时间点上,出现了⼀个JavaScript异
常错误,⽐如⼀个TypeError或ReferenceError,这个异常都会被捕捉,并且会使这个Promise被拒绝。
var p = new Promise(function (resolve, reject) { foo.bar(); // foo未定义 resolve(2);})p.then(function (data) { console.log(data);// 永远也不会到达这⾥}, function (err)
从以上⼏点可以明确,Promise可以解决⼀系列控制反转带来的回调信任问题,但是Promise并没有完全摆脱回调,⽽是把回调的位置放到
了then中,换成了决议通知,这样其实说到底顺序问题还是没有解决。
解决顺序问题--Generator
⽣成器 (Generator)
⽣成器是⼀种返回迭代器的函数,通过 function 关键字后的 * 号来表⽰。
迭代器(Iterable)
迭代器是⼀种对象,它具有⼀些专门为迭代过程设计的专有接⼝,所有迭代器对象都有⼀个 next ⽅法,每次调⽤都返回⼀个结果对
象。结果对象有两个属性,⼀个是 value,表⽰下⼀个将要返回的值;另⼀个是 done,它是⼀个布尔类型的值,当没有更多可返回数据时返回 true。迭代器还会保存⼀个内部指针,⽤来指向当前集合中值的位置,每调⽤⼀次 next() ⽅法,都会返回下⼀个可⽤的值。
⽣成器⼀般使⽤形式
function *foo() { var x = yield 2; var y = x * (yield x + 1) console.log( x, y ); return x + y}var it = foo();it.next() // {value: 2, done: (3) // {value: 4, done: fals
yield.. 和 next(..) 这⼀对组合起来, 在⽣成器的执⾏过程中构成了⼀个双向消息传递系统。
⼀般来说,需要的 next(..) 调⽤要⽐ yield 语句多⼀个,前⾯的代码⽚段有两yield 和三个 next(..) 调⽤;
第⼀个 next(..)是⽤来启动⼀个⽣成器,并运⾏到第⼀个 yield 处;
每个 yield.. 基本上是提出了⼀个问题:“这⾥我应该插⼊什么值?”,这个问题由下⼀个 next(..) 回答。 第⼆个 next(..) 回答第⼀个yield.. 的问题,第三个 next(..) 回答第⼆个 yield 的问题,以此类推;
yield.. 作为⼀个表达式可以发出消息响应 next(..) 调⽤, next(..) 也可以向暂停的 yield 表达式发送值。
异步迭代⽣成器
来看⼀下下⾯这段代码,我们在⽣成器⾥ yeild 请求函数(暂停⽣成器继续执⾏,同时并执⾏请求函数),执⾏⽣成器产成可迭代对象后,⼜
在请求函数⾥通过 next() ⽅法获取到请求结果、将结果传进⽣成器并恢复⽣成器的执⾏。
//setTimeout 模拟ajax请求var it=nullfunction foo(){ let firstData='firstData' setTimeout(function(){ it.next(firstData) },500)}function goo(data){ let secondData='secon
我们在⽣成器内部有了看似完全同步的代码(除了 yield 关键字本⾝),但隐藏在背后的是,在 foo(..) goo(...)内的运⾏可以完全异步。在可
读性和合理性⽅⾯也都是⼀个巨⼤的进步。
从本质上⽽⾔,我们把异步作为实现细节抽象了出去,使得我们可以以同步顺序的形式追踪流程控制:“发出⼀个 Ajax 请求,等它完成之
后打印出响应结果。”并且只在这个流程控制中表达了两个步骤,⽽这种表达能⼒是可以⽆限扩展的,以便我们⽆论需要多少步骤都可以表
达。
由以上来看,顺序问题也得到了解决
es7的解决⽅案 Async/Await
上⾯介绍的Promise和Generator,把这两者结合起来,就是Async/Await。
Generator的缺点是还需要我们⼿动控制next()执⾏,使⽤Async/Await的时候,只要await后⾯跟着⼀个Promise,它会⾃动等到
Promise决议以后的返回值,resolve(...)或者reject(...)都可以。
Async/Await是Generator的语法糖,和generator的写法很像,就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。
它有以下优点:
内置执⾏器:Generator函数的执⾏必须靠执⾏器,所以才有了 co 函数库,⽽ async 函数⾃带执⾏器.也就是说,async 函数的执
⾏,与普通函数⼀模⼀样。
更好的语义:async 和 await,⽐起星号和 yield,语义更清楚了。async 表⽰函数⾥有异步操作,await 表⽰紧跟在后⾯的表达式需要等待结果。
async 的作⽤
async 函数负责返回⼀个 Promise 对象,如果在async函数中 return ⼀个直接量,async 会把这个直接量通过solve() 封装
成 Promise 对象;如果 async 函数没有返回值,它会返回 solve(undefined)
await在等待什么
⼀般我们都⽤await去等带⼀个async函数完成,不过按语法说明,await 等待的是⼀个表达式,这个表达式的计算结果是 Promise 对象或
者其它值,所以,await后⾯实际可以接收普通函数调⽤或者直接量。
如果await等到的不是⼀个promise对象,那跟着的表达式的运算结果就是它等到的东西。
如果是⼀个promise对象,await会阻塞后⾯的代码,等promise对象resolve,得到resolve的值作为await表达式的运算结果
虽然await阻塞了,但await在async中,async不会阻塞,它内部所有的阻塞都被封装在⼀个promise对象中异步执⾏
Async Await使⽤场景
当需要⽤到promise链式调⽤的时候,就体现出Async Await的优势;
function delayTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), n); });}f unction step1(n) { console.log(`step1 with ${n}`); return delayT await 若等待的是 promise 就会停⽌下来
例如:业务是这样的,我有三个异步请求需要发送,相互没有关联,只是需要当请求都结束后将界⾯的 loading 清除掉即可。
之前⽤callback这么写
let lock=0ajax(function(){ lock++ if(lock===3){ clearLoading()// 清除loading }})
现在⽤ Async/Await
// ajax⽅法返回的是promise对象async function doIt(){ await ajax(1) await ajax(2) await ajax(3) clearLoading()}
loading 确实是等待请求都结束完才清除的。但是观察下浏览器的 timeline 请求是⼀个结束后再发另⼀个的
正常应该是利⽤Promise.all + async
async function doIt(){ let p1=ajax(1) let p2=ajax(2) let p3=ajax(3) await Promise.all([p1,p2,p3]); clearLoading()}
所以async-await并不能取代promise,两者应该是相互配合,才是⽬前较好的JavaScript异步解决⽅案。
欢迎⼤家关注“58架构师”,定期分享云计算、AI、区块链、⼤数据、搜索、推荐、存储、中间件、移动、前端、运维等⽅⾯
的前沿技术和实践经验。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。