跨越时空的对白——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教程,还以为是我当年看书走眼了,忘了啥,查漏补缺,结果阮老师就这么轻飘飘一句话
┑( ̄Д  ̄)┍
时间和空间上的分离
阮老师,您说的是平行时空么?还是错位空间?
我吹过你吹过的晚风
那我们算不算 相拥
我遇到过你发现的error,那我们算不算相拥,反正我读完也是挺郁闷的,阮老师那种在大气层的理解,对于普通人的我还是需要一层层剖析才能理解,那就先按照自己的理解来说吧,大家一起探讨一下,看看有没有道理
我们知道对于nodejs
的异步实现都是借助libuv
其他线程完成的。正常情况下,当eventloop
通知调用栈处理异步回调函数的时候,原调用栈种的函数应该已经执行完了,因此调用函数和异步逻辑是由完全不同的线程执行的,本质上是没有交集的,这个时候可以理解为空间上是隔离的。异步回调被触发执行时,调用函数早已执行结束,因而,回调函数和调用函数的执行在时间上也是隔离的
好了,时空隔离的问题,勉强解释通了,但是async...await...
又是怎么打破这种隔离,让其中的try...catch...
可以捕获到异步操作中的异常?曾经大胆猜测,async...await...
可以强行拉长try...catch...
作用域,让调用函数的生命周期可以尽量延长,以至于可以等待直到异步函数执行完成,在此期间如果异步过程出现异常,调用函数就可以捕捉到,然而这个延长函数生命周期并等待异步执行结束,这不就是相当于是在阻塞线程的执行?阻塞执行——这跟JS的非阻塞的特质又是背道而驰的。
至此我总觉得在调用函数和异步逻辑之间存在某种诡异的tunnel,对!说的就是那股风!其可以在主函数和异步函数这两个不同时空互相隔离的生物进行消息传递,比如说在时空A中捕获了时空B里面的异常消息,这样它们就可以相拥❤
怎么想都觉得这个过程离大谱!
try...catch...不能捕获异步异常
try...catch...
能捕获到的仅仅是try
模块内执行的同步方法的异常(try执行中且不需要异步等待),这时候如果有异常,就会将异常抛到catch
中。
除此之外,try...catch...
执行之前的异常,以及try...catch...
内的异步方法所产生的异常(例如ajax请求、定时器),都是不会被捕获的!看代码
这段代码中,setTimeout
的回调函数抛出一个错误,并不会在catch
中捕获,会导致程序直接报错崩掉。
这说明在js
中try...catch...
并不是说写上一个就可以高枕无忧。尤其是在异步处理的场景下。
那这个问题是怎么来的呢?
我从网上扒了个动图,可以比较形象的解释这个问题。图中演示了foo
,bar
,tmp
,baz
四个函数的执行过程。同步函数的执行在调用栈中转瞬即逝,异步处理需要借助libuv
。比如这个setTimeout
这个Web API,它独立于主线程中的libuv
中别的线程负责执行。执行结束吼,会将对应回调函数放到等待队列中,当调用栈空闲吼会从等待队列中取出回调函数执行
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
捕获
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
一起使用
code3
function bar()
return new Promise((resolve, reject)=>
setTimeout(()promise和async/await