跨越时空的对白——async&await分析

Posted yerikyu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跨越时空的对白——async&await分析相关的知识,希望对你有一定的参考价值。

同步中的异步

在ES6中新增了​​asgnc...await...​​的异步解决方案,对于这种方案,有多种操作姿势,比如这样

const asyncReadFile = async function()
const f1 = await readFile(/etc/fstab)
const f2 = await readFile(/etc/shells)
console.log(f1.toString())
console.log(f2.toString())

或者是这样

async function f()
try
await new Promise.reject(出错了)
catch(e)


return await Promise.resolve(hello yerik)

是否能发现这两种使用方式的各自的特点:

  • ​async...await...​​异步解决方案支持同步的方式去执行异步操作
  • ​async...await...​​​异步解决方案支持通过​​try...catch...​​进行异常捕获

对于第一点来说还好理解,但第2种说法就很费解了,以至于有一种颠覆以往理解的绝望感,对于js的世界观都已经灰色。对于​​try...catch...​​​来说,不都是同步执行过程中捕获异常的吗,为何在​​async...await...​​​中的​​try...catch...​​可以捕获异步执行的异常呢?

这个时候就去翻一下阮一峰老师的ES6教程,还以为是我当年看书走眼了,忘了啥,查漏补缺,结果阮老师就这么轻飘飘一句话

跨越时空的对白——async&await分析_异步操作

┑( ̄Д  ̄)┍

时间和空间上的分离

阮老师,您说的是平行时空么?还是错位空间?

跨越时空的对白——async&await分析_异步操作_02

我吹过你吹过的晚风

那我们算不算 相拥

我遇到过你发现的error,那我们算不算相拥,反正我读完也是挺郁闷的,阮老师那种在大气层的理解,对于普通人的我还是需要一层层剖析才能理解,那就先按照自己的理解来说吧,大家一起探讨一下,看看有没有道理

我们知道对于​​nodejs​​​的异步实现都是借助​​libuv​​​其他线程完成的。正常情况下,当​​eventloop​​通知调用栈处理异步回调函数的时候,原调用栈种的函数应该已经执行完了,因此调用函数和异步逻辑是由完全不同的线程执行的,本质上是没有交集的,这个时候可以理解为空间上是隔离的。异步回调被触发执行时,调用函数早已执行结束,因而,回调函数和调用函数的执行在时间上也是隔离的

好了,时空隔离的问题,勉强解释通了,但是​​async...await...​​​又是怎么打破这种隔离,让其中的​​try...catch...​​​可以捕获到异步操作中的异常?曾经大胆猜测,​​async...await...​​​可以强行拉长​​try...catch...​​作用域,让调用函数的生命周期可以尽量延长,以至于可以等待直到异步函数执行完成,在此期间如果异步过程出现异常,调用函数就可以捕捉到,然而这个延长函数生命周期并等待异步执行结束,这不就是相当于是在阻塞线程的执行?阻塞执行——这跟JS的非阻塞的特质又是背道而驰的。

至此我总觉得在调用函数和异步逻辑之间存在某种诡异的tunnel,对!说的就是那股风!其可以在主函数和异步函数这两个不同时空互相隔离的生物进行消息传递,比如说在时空A中捕获了时空B里面的异常消息,这样它们就可以相拥❤

怎么想都觉得这个过程离大谱!

跨越时空的对白——async&await分析_异步分析_03

try...catch...不能捕获异步异常

​try...catch...​​​能捕获到的仅仅是​​try​​​模块内执行的同步方法的异常(try执行中且不需要异步等待),这时候如果有异常,就会将异常抛到​​catch​​中。

跨越时空的对白——async&await分析_协程_04

除此之外,​​​try...catch...​​​执行之前的异常,以及​​try...catch...​​内的异步方法所产生的异常(例如ajax请求、定时器),都是不会被捕获的!看代码

跨越时空的对白——async&await分析_异步分析_05

这段代码中,​​​setTimeout​​​的回调函数抛出一个错误,并不会在​​catch​​中捕获,会导致程序直接报错崩掉。

这说明在​​js​​​中​​try...catch...​​并不是说写上一个就可以高枕无忧。尤其是在异步处理的场景下。

那这个问题是怎么来的呢?

我从网上扒了个动图,可以比较形象的解释这个问题。图中演示了​​foo​​​,​​bar​​​,​​tmp​​​,​​baz​​​四个函数的执行过程。同步函数的执行在调用栈中转瞬即逝,异步处理需要借助​​libuv​​​。比如这个​​setTimeout​​​这个Web API,它独立于主线程中的​​libuv​​中别的线程负责执行。执行结束吼,会将对应回调函数放到等待队列中,当调用栈空闲吼会从等待队列中取出回调函数执行

跨越时空的对白——async&await分析_协程_06

const foo = ()=>console.log(Start!)
const bar = ()=>setTimeout(()=>console.log(Timeout!), 0)
const tmp = ()=>Promise.resolve(Promise!).then(res=>console.log(res))
const baz = ()=>console.log(End!)

foo();
bar();
tmp();
baz();

不能捕获的原因

为了讲清楚不能被捕获的原因,我改一下代码,模拟异步过程发生了异常。大家可以把执行逻辑再套回刚才的动图逻辑再看一下,(后面有机会学习怎么做动图哈哈哈)

const bar = ()=> 
try
setTimeout(()=>
throw new Error()
, 500)
catch(e)
// catch error.. dont work

当​​setTimeout​​​的回调在​​Queue​​​排队等待执行的时候,​​Call Stack​​​中的​​bar​​​就已经执行完了,​​bar​​​的销毁顺便也终止了​​try...catch...​​​的捕获域。当主进程开始执行​​throw new Error()​​的时候,相当于外层是没有任何捕获机制的,该异常会直接抛出给V8进行处理


回调函数无法捕获?

因为大部分遇到无法​​catch​​​的情况,都发生在回调函数,就认为回调函数不能​​catch​​,这个结论是对的吗?

只能说不一定,且看这个例子

// 定义一个 fn,参数是函数。
const fn = (cb: () => void) =>
cb();
;

function main()
try
// 传入 callback,fn 执行会调用,并抛出错误。
fn(() =>
throw new Error(123);
)
catch(e)
console.log(error);


main();

结果当然是可以​​catch​​​的。因为​​callback​​​执行的时候,跟​​main​​​还在同一次事件循环中,即一个​​eventloop tick​​​。所以上下文没有变化,错误是可以​​catch​​的。 根本原因还是同步代码,并没有遇到异步任务。


如何捕获?

简单来说就是哪里抛异常就在哪里捕获

const bar = ()=> 
setTimeout(()=>
try
throw new Error()
catch(e)
// catch error.. dont work

, 500)

那这样写代码一点都不会快乐了,要出处小心,时候留意以防哪里没有考虑到异常的场景。

基于Promise的解决方案

所谓​​Promise​​​,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,​​Promise​​​ 是一个对象,从它可以获取异步操作的消息。​​Promise​​提供统一的 API,各种异步操作都可以用同样的方法进行处理。

本质上,这个就是一个状态管理机,同时又提供​​resolve​​​和​​reject​​​两个开关。​​resolve​​​负责将状态机的状态调整成​​Fulfilled​​​,​​reject​​​将状态处理成​​Rejected​​。

对于​​Promise​​来说是如何处理异常的?我们不妨通过改造前面的代码来试试


code1

function bar()
new Promise((resolve, reject)=>
setTimeout(()=>
// 通过throw抛出异常
throw new Error(err)
, 500)
)

function exec()
try
bar().then(res=>
console.log(res, res)
)
catch(err)
console.log(err has been caught in try-catch block)

在这个过程中,尝试抛出全局异常​​Uncaught Error​​​,然而​​try...catch...​​​并没有捕获到。造成这个问题的原因还是在于异常抛出的时候,​​exec​​​已经从执行栈中出栈了,此外,在​​Promise​​​规范里有说明,在异步执行的过程中,通过​​throw​​​抛出的异常是无法捕获的,异步异常必须通过​​reject​​捕获

跨越时空的对白——async&await分析_异步分析_07

code2

function bar()
return new Promise((resolve, reject)=>
setTimeout(()=>
reject(err)
, 500)
)

function exec()
try
bar().then(res=>
console.log(res, res)
)
catch(err)
console.log(err has been caught in try-catch block)

这次通过​​reject​​​抛出异常,但是​​try...catch...​​​同样还是没有捕获到异常。原因是​​reject​​​需要配合​​Promise.prototype.catch​​一起使用

跨越时空的对白——async&await分析_异步操作_08

code3

function bar()
return new Promise((resolve, reject)=>
setTimeout(()promise和async/await

promise和async/await

promise和async/await

promise和async/await

对python async与await的理解

AsyncLocal 与 async await