JavaScript 三招两式之异步编程 (下)

953 阅读5分钟

值此Node.js 8.x版本晋升为LTS之际,半年后完成此下篇上篇传送门,Node 8+版本对async,await的原生支持,让js开发者对异步逻辑的开发变得更加方便。

对于Javascript这门语言的异步逻辑编程方式而言,promise是一个对函数式编程方式更友好(相对回调而言) 但并没有解决异步代码的复杂程度和调试难问题,这正是ECMAScript7草案落地所带来的新语法特性async要解决的问题。

先来个简单的例子:

首先利用promise封装了一个promise版的setTimeout延迟方法,然后注意async函数必须在声明时显式标识async方法, 这个标识意味着此函数内部允许使用await关键字。

运行代码,会发现在一个async函数内部,每一个await关键字将会暂停代码的执行直至promise resolve之后继续执行, 注意,被暂停执行的只是当前这个方法,而不是整个Node进程。当然这一切有个前提就是await后面接的一定是一个 返回promise对象的函数,如若不然await关键字就不起任何作用,函数返回值会直接返回给await前面的赋值变量。

值得注意的是,async函数自己也会把自己封装成一个promise,因此我们可以这么做:

但是目前的async有一个缺点,就是无法接受promise的reject,如果promise返回了reject,await是无法捕获错误的, 一旦你await的promise在某种情况下返回reject结果,程序会报错“Unhandled promise rejections are deprecated”。

解决的办法是对所有await外面包装一层try-catch,reject的promise都会被catch到,然后你可以执行异常处理。

有关await还有一个使用技巧,就是他可以和循环语句结合起来使用,像这样:

当你在写一个网络请求代码时,如果你是在做类似抢票这样的请求,你不知道当前请求是否能抢到票,并且如果请求失败了你也不知道再 发多少次请求就能够成功,你希望程序永无休止的执行“抢票请求”这个异步的操作直到抢到票为止,这时await的存在可以帮你轻松的解决 这个棘手的问题,如果没有await,你只能用promise+递归的方式去写一段复杂的难以调试的代码了。

async并没有让js这门语言丧失其在异步逻辑方面的先天优势,只不过是一层语法糖,让你可以以同步代码来编写异步逻辑,你依然可以借助 promise.all和promise.race方法去实现多个异步逻辑的并发,而没必要一个一个地执行异步逻辑。js在这方面, 依然是具备“以较为简单的代码逻辑, 较小的内存占用(eventloop而非java python那样起一个thread)的方式实现异步非阻塞的并发逻辑”的优势的。

await的代码暂停执行,其实质是在js中引入了协程(coroutine),这在ES6中的generator时期就已被引入。 不过我认为await对js的异步 代码编写提供了更完美的支持。

以上便是js这门语言中所有处理异步逻辑的原生api支持,涉及到promise方面的技术如果你不了解可以去看看上篇

在程序设计方面还有一点是值得思考的:

如何控制一个node进程执行异步逻辑的并发度

在单个node进程中存在大量并行的异步函数(例如你封装了网络请求方法,并且并发大量网络请求),如果并发度没有一个可控的上限的话, 在高负荷环境下网络请求的成功率会大大下降,最先报出的一些错误可能是http.client模块底层的C++ error, 这依据你的node运行环境 的网卡驱动不同而异。

解决方式有两个思路:一是 实现一个请求池,在js在内存里维护一个请求队列和当前并发值, 当并发度到达阈值时就暂时部发起更多网络请求, 而是等待正在进行的网络请求成功返回时再发起更多网络请求, 而在自己封装的网络请求方法里,先不发起网络请求,而是走整个请求池的调度,所有网络请求都调用自己封装的这个网络请求方法。

二是以多进程并发的方式,每个进程都是一个独立的v8 isolate,拥有自己的GC运行时和event loop环境,master进程自身不做请求, 而是靠fork新的进程出来执行网络请求,所以并发度的控制实际上就是控制fork出的进程数量。

第一个方法较为轻巧,内存占用小,性能好(不存在进程间通信问题,也不涉及到锁的问题),但稳定性差,且在复杂的业务场景下 难以定位和调试;第二个方法比较重,企业级应用往往需要结合redis等做队列,但是运行稳定,每一个进程之间相互隔离, 并发程序完全可扩展,甚至分布式扩展为一整个集群。

在文章最后,欢迎大家关注下我最近在搞的一个开源项目webster, 项目以Node.js 结合Chrome headless模式实现了一个高可用性网络爬虫抓取框架,借以chrome对页面的渲染能力, 可以抓取一个页面中 所有的js及ajax渲染的异步内容。