为什么async/await关键字是如此重要

现在JS里有async/await了,处理异步代码几乎不再有什么争议,但还是会有人有疑问,为什么不把所有函数都定义成async的,然后所有函数调用都写成await的,这样最终不就可以省略掉所有的async/await关键字了吗(默认隐式async/await)?这样不就达成了“天下无异步”的太平盛世了吗?

只要稍微动点脑筋就不会有这种想法。

我们都知道目前的环境下JS它还是一门单线程的语言,然后通过Event Loop来实现异步IO。虽然也有fibjs这种“异类”,会稍微打破一些认知。基于这个前提,我们就有一些共识,比如:

  • 同一个event里的代码是顺序执行的不可分割的单元,在这里就不需要考虑资源竞争的问题了。
  • 通过callback或者promise方式调用的东西会受到Event Loop的调度,不管它是Macro Task还是Micro Task,反正会进入另一个单元里执行。

那么有如下代码

1
2
3
4
5
6
7
8
9
function A() {
foo()
bar()
}

async function B() {
await foo()
await bar()
}

如果单看这两个函数,如果ABfoobar都没有副作用,那么会觉得这两个函数的效果没什么差别,在这种情况下“默认async/await”似乎是可行的。

但如果有共享资源和竞争,事情就会变得完全不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
var shared = 0

function A() {
foo()
bar()
return shared++
}

async function B() {
await foo()
await bar()
return shared++
}

如果AB用到了共享资源,对于A而言,因为是完全同步执行的,那么整个A的代码会在一个event里执行,它是“线程安全”的(这里加引号的是因为这个不是严格的线程安全的概念,只是表示个意思),只要通过静态代码分析的手段就可以得知foobarshared有没有副作用,那么这个A函数的执行结果就是可预知的。

B则完全不一样,因为await关键字会出让执行权,也就是foobarreturn不在一个event里执行,那么在这三行代码的“行缝儿”之间就有无数的可能性,这些缝隙里塞进去一万个event也不得而知。这种情况下对foobar执行静态分析(去判断他们对shared有没有副作用)是没什么卵用的,因为shared被修改的可能性有无数种,比如触发了一个事件导致别的listener修改了shared。也就是说B函数的执行结果是不能通过静态分析而预知的,它不再是纯函数了(废话)。

这就是async/await的重要性了,它绝对不是一个简单的语言设计的品味问题,不全局省略async/await是因为它明确的告诉写代码的人这个地方会发生什么事情。开发者只要看到它,马上就会对这里的共享资源多提一个心眼,会以完全不一样的眼光去看待B函数。

而对于严肃地写代码、写严肃的代码而言,“知道一行代码会发生什么”这件事有多重要我想不需要再多强调了。