js异步等待完成后再进行下一步操作_JavaScript 异步详解
点击上方蓝字 关注我们!
相关概念
1 单线程&非阻塞I/O
JavaScript语言本身是单线程的。什么是单线程?可以理解为一次只能执行一个任务,所有的任务在执行开始前排成一个队列,等待顺序执行。
为什么是单线程? 如果JS是多线程的,一个线程在某个DOM节点上添加内容,同时另一个线程删除了这个节点,那浏览器以谁为准?所以为了避免复杂性,JavaScript从诞生起就是单线程的。
非阻塞I/O:I/O即Input/Output,非阻塞和阻塞的区别就在于在系统接收输入到输出期间,能不能接收其他的输入。
这里举一个例子:食堂排队打饭 / 餐厅点餐食堂排队打饭: 我们排成一队打饭,阿姨为排到的人打饭( Input )时,是不会理会 后边的人想要什么的,直到给当前的人打完餐( Output )后,才会接受下一个人的需 求( 下一个Input ),这就是阻塞I/O餐厅点餐:我们进入餐厅后,服务员来为我们点餐( Input ),点餐结束后,服务员 将菜单传给后厨(扔进 任务队列 ----后边会详细介绍);接着服务员会为下一个人点餐 ( 下一个Input ),直到后厨做好,服务员根据餐桌位置把菜端上来( Output );在 此期间,服务员不断地点餐,送餐,后边的人不需要等待前边的人上完菜再点餐,这就是非阻塞I/O
因为非阻塞I/O的特性,也造就了JS能够高效率地执行代码,也能够承载高并发!
2 JS异步
异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念,我们来看下面这张图:
同步的流程中,所有任务需要等待上一个任务完成后才能开始执行; ----- 同步流程中,总执行时间是所有同步任务执行时间的总和。 而异步的任务则不需要等待前边的任务执行完成,就可以开始本次任务的执行,任务之间互 相不受干扰; ---- 异步流程中,总执行时间是耗时最长的异步任务所需时间。对于JS来说,执行代码过程中有能够 的操作(比如声明、赋值、循环等,也称同步任务),还有一些的操作(比如定时器、网络请求、文件读写、事件监听等,也称异步任务),如果让他们像前边的任务一样老老实实地等待,这样对于单线程的JS来说执行效率就非常低,甚至会形成假死状态。
所以JS的宿主环境(浏览器、Node.js)会为这些耗时的操作单独开辟线程,比如网络请求线程、 定时器触发线程、浏览器事件触发线程、文件读写线程等,等这些任务被其他线程执行完成后,再通过回调的方式返回,这就是JS异步。
3 事件循环(Event Loop)
JS主线程顺序读取代码,形成一个执行栈(execution context stack) ;
碰到耗时的操作,也就是需要异步执行的代码,主线程就根据异步类型分派给其他的异步线程去处理这些操作,当他们处理完成后,将结果扔给任务队列;
当主线程把所有执行栈中的内容执行完成后,开始循环读取任务队列中,有完成的,就把这些异步的回调(callback)拉到主线程继续执行,如此反复,称为事件循环(Event Loop)
Java异步处理的历程
1 callback
最开始,我们基本都是通过函数传参callback回调函数来解决异步问题;
来看下面这段代码,假设我们请一位先生吃饭,就会有:
执行一下,控制台告诉我们结果是hungry。白吃了?其实并不是,这就相当于人刚说要开始吃,还没动筷子呢,就问人吃饱没,那不是很不礼貌么?
所以,得礼貌些,一定要在人吃完了再问。这里我们用callback回调函数的方式来问:
再次执行,那指定是饱了 full ,因为是在人家吃完以后才问的。
我们再假设另外一种情况,如果这位先生饭量比较大,一碗饭根本不够吃,最终吃几碗能饱完
全看人心情,但是我们的钱包只够吃三碗饭的,实在吃不饱也没办法了:
先不管这个人吃没吃饱,我们看代码,三碗吃饭下来,一层套一层的回调函数+条件判断,代
码就显得很乱。这里逻辑还算是简单的,如果碰到复杂的,或者嵌套层数更多的情况,代码维
护起来就很困难,这就是经典的回调地狱( callback hell )问题。下面引入Promise来解决回调
地狱。
2Promise
Promise是 es6 中很重要的一个概念,也是我们最常用的异步解决方案。它是一个构造函数,
所以我们需要使用 new 关键字来创建一个Promise:
Promise译为承诺,表示承诺在未来有一个确切的答复;分别有以下三个状态,也称为状态机:
我们实例化一个Promise的时候,它就会进入pending状态,直到我们告诉它是成功 resolve() 还
是失败 reject() ,Promise才会改变状态:
resolve 的内容会走到 then 回调中, reject 的内容会走到Promise后的第一个 catch (后边会
介绍)中,不管成功失败,都会走一个 finally :
解决回调地狱,我们拟定三轮面试,三轮面试全部成功才算成功,否则就是失败:
看得出来,所有的回调变成了链式调用,错误捕捉只需要一个 catch 拦截即可,这样大大提高了
代码的可读性和可维护性。
并发异步问题:假定一个场景,我们同时面试多家公司,只有都成功了才说明咱是大牛,否则就
是菜鸡;正常思维我们会开启一个计数器,面试通过就对计数器 +1 ,最后等待一定时间,再通
过判断计数器是否到达面试总数来决定自己是菜鸡还是大牛:
这是我们知道每个 Promise 的结束时间,最终等待超过最长的即可,但是如果每轮面试的时
间都是未知的呢,怎么在最后做判断? 往下看
引入 Promise.all() 解决异步并发问题, Promise.all([]) 接受一个由 Promise 作为参数,当参
数中所有的 Promise 都成功后才会进入 Promise.all 的 then 回调中,并且会将三个 resolve
返回值作为数组返回给 Promise.all 的 then ,否则都会进入 catch 回调中:
这样一来,不管每个面试多长时间,我们都能清晰地判断是否都完事了。
3async/await
async/await可以让我们用同步的思维去编写异步代码,被称为JS异步问题的终极解决方案
async 是修饰 function 的关键字, async function 其实是一个 Promise 的语法糖。观察下
面代码,我们执行 async function 返回的结果就是一个 Promise:
await 是 async functon 中的一个关键字,用来阻止后边的代码立即执行,并且可以用同步的
方法获取到 Promise 的执行结果:
执行结果是 full ,但是看代码,并没有在回调中打印状态,而是直接在外部打印,并且能得到full
的状态,是因为 await 关键字起了作用,它的存在让异步编程回归到了同步。
那么用 async/await 的方式来写三轮面试的方式呢:
这段用同步的方式写出来的异步代码,我们拿来运行一下,就能够清晰地知道面试的过程,包括哪
一轮通过,到第几轮失败。注意:我们正常在外部是无法使用 try/catch 来捕捉 Promise 返回的 error ,但是使用async/await 就
可以轻松在外部捕捉到。
总结
1优先级
介绍了三种异步解决方案,那么我们在编码过程中应该如何选择呢?
推荐优先级: async/await > Promise > callback
Promise > callback
Promise 的链式调用能够让我们用线性思维去编写代码;
Promise 能够解决 callback 方式所产生的回调地狱问题;
;
async/await > Promise
async/await
允许我们用更容易理解的同步思维去编写代码;
async/await
能够通过
try/catch
来捕捉
Promise
只能在
catch()
中捕获的错误;
通过
async/await
解决三轮面试的案例可以看出,
async/await
在
接受中间值
方面表现得更加
优秀;
;
2兼容性
因为 Promise是语法,async/await是语法,所以兼容性方面还是有所欠缺,以下是它们在
can i use 网站中的表现:
but,这些问题在我们编码的时候可以通过构建工具来解决,如 babel 可以把我们的高版本JS
代码转换成浏览器能够通吃的兼容性代码。
结束语
异步的问题一直是JS三座大山(原型与原型链,作用域及闭包,异步和
单线程)之一,平常编码也经常碰到,希望本文能对大家有所帮助!
喜欢就点个在看再走吧