背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效。

已读完书籍《架构简洁之道》。

当前阅读周书籍《深入浅出的Node.js》

异步编程

函数式编程

函数的灵活性是JavaScript比较吸引人的地方之一,无论调用它,或者作为参数,或者作为返回值均可。

高阶函数

函数的参数一般接受基本的数据类型或是对象引用,返回值也是基本数据类型和对象引用。

高阶函数则是可以把函数作为参数,或是将函数作为返回值的函数。

如下代码所示,将函数作为了返回值:

function foo(x) {
  return function () {
    return x;
  };
}

偏函数用法

偏函数用法是指创建一个调用另外一个部分——参数或变量已经预置的函数——的函数的用法。

在下面的代码中,通过isType()函数预先指定type的值,然后返回一个新的函数:

var isType = function (type) {
  return function (obj) {
    return toString.call(obj) == '[object ' + type + ']';
  };
};

var isString = isType('String');
var isFunction = isType('Function');

异步编程的优势与难点

优势

Node带来的最大的优势是基于事件驱动的非阻塞I/O模型,非阻塞I/O可以使CPU与I/O并不相互依赖等待,让资源得到更好的利用。

阅读周·深入浅出的Node.js | 有异步I/O,必有异步编程_异步调用

难点

1、难点1:异常处理

异步方法通常在第一个阶段提交请求后立即返回,因为异常并不一定发生在这个阶段,try/catch的功效在此处不会发挥任何作用。

Node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常抛出:

async(function (err, results) {
  // TODO
});

2、难点2:函数嵌套过深

Node中,事务中常存在多个异步调用的场景。

如下代码,虽然结果的保证上是没有问题的,但是这样的实现并没有利用好异步I/O带来的并行优势。

fs.readFile(template_path, 'utf8', function (err, template) {
  db.query(sql, function (err, data) {
    l10n.get(function (err, resources) {
      // TODO
    });
  });
});

3、难点3:阻塞代码

JavaScript中没有sleep()这样的线程沉睡功能,用于延时操作的只有setInterval()和setTimeout()这两个函数。但这两个函数并不能阻塞后续代码的持续执行。

通常开发者会采用如下的方式达到阻塞的效果:

// TODO
var start = new Date();
while (new Date() - start < 1000) {
  // TODO
}
// 需要阻塞的代码

但是实际上这段代码会持续占用CPU进行判断,与真正的线程沉睡相去甚远,完全破坏了事件循环的调度。

4、难点4:多线程编程

对于服务器端而言,如果服务器是多核CPU,单个Node进程实质上是没有充分利用多核CPU的。

浏览器提出了Web Workers,它通过将JavaScript执行与UI渲染分离,可以很好地利用多核CPU为大量计算服务。同时前端Web Workers也是一个利用消息机制合理使用多核CPU的理想模型。借助Web Workers的模式,开发人员要更多地去面临跨线程的编程。

5、难点5:异步转同步

Node提供了绝大部分的异步API和少量的同步API,偶尔出现的同步需求将会因为没有同步API让开发者突然无所适从。目前,Node中试图同步式编程,但并不能得到原生支持,需要借助库或者编译等手段来实现。

异步编程解决方案

异步编程的主要解决方案有如下3种:

事件发布/订阅模式

事件监听器模式是回调函数的事件化,又称发布/订阅模式。Node自身提供的events模块是发布/订阅模式的一个简单实现,Node中部分模块都继承自它,这个模块比前端浏览器中的大量DOM事件简单

Promise/Deferred模式

Promise/Deferred模式,是一种先执行异步调用,延迟传递处理的方式。

Promise/Deferred模式在一定程度上缓解了深度嵌套让编程的体验变得不愉快的问题。

流程控制库

一些非模式化的应用,虽非规范,但更灵活。

1、尾触发与Next

尾触发方法是需要手工调用才能持续执行后续调用的,常见的关键词是next。尾触发目前应用最多的地方是Connect的中间件。

2、async

async,流程控制模块。在Node开发中,流程控制是开发过程中的基本需求。async模块提供了20多个方法用于处理异步的各种协作模式。

3、Step

流程控制库Step,它比async更轻量,在API的暴露上也更具备一致性,因为它只有一个接口Step。

4、wind

wind,前身为Jscex。它为JavaScript语言提供了一个monadic扩展,能够显著提高一些常见场景下的异步编程体验。

异步并发控制

在Node中,可以利用异步发起并行调用。

对于异步I/O,虽然并发容易实现,但是依然需要控制。需要给予一定的过载保护,以防止过犹不及。

异步调用的并发限制在不同场景下的需求不同:非实时场景下,让超出限制的并发暂时等待执行已经可以满足需求;但在实时场景下,需要更细粒度、更合理的控制。

总结

我们来总结一下本篇的主要内容:

  • 异步编程的优势是基于事件驱动的非阻塞I/O模型,非阻塞I/O可以使CPU与I/O并不相互依赖等待,让资源得到更好的利用。
  • JavaScript异步编程的难题已经基本解决,无论是通过事件,还是通过Promise/Deferred模式,或者流程控制库。
  • 在掌握以上技巧之后,异步编程不是难事,习惯异步编程之后,将会收获许多值得享受的编程体验。

作者介绍非职业「传道授业解惑」的开发者叶一一。《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。