如何理解NodeJS

(二)写好异步代码

链接:(一)基本原理

上篇说到,得益于异步的机制,NodeJS在拥有卓越性能的同时,也会使逻辑的实现变得更复杂和难于理解。

首先,其实NodeJS也是能实现blocking的代码的,比如读取一个文件:

var fs = require("fs");

var contents = fs.readFileSync("data.txt", "utf8");

console.log(contents);

console.log("done");

这样写的输出结果就是先读取打印文件的内容,再输出done.

当然,这样就失去了使用NodeJS的优势,而且,绝大部分的库(比如文件的读取,数据库的操作等)都是使用异步机制,并且不提供类似fs的sync同步方法,而一般的Callback写法应该是这样的:

var fs = require("fs");

fs.readFile("data.txt", "utf8", function(err, contents) {
    if(err) {
        errorHandler();
    }
    console.log(contents);
});

console.log("done");

需要注意的是,这样的写的话,输出的顺序就是done之后再是文件的内容了。

Callback总结:NodeJS一般的异步实现方式

someThing.someFunction(param...,callback);

用通俗的话解释就是:因为是异步,所以当你派发任务的时候,请直接告诉我这件事完成之后该干什么,而不是等我做完了之后再告诉我。

请牢牢记住Callback形式的异步,后面给出的Promise,await等异步处理方式从某种意义上来说都是Callback的语法糖,让代码更漂亮更好读而已。


Callback形式代码的问题是代码会写得非常难看,无数层的嵌套,无数的缩进,变量作用域的混淆。这就是所谓的Callback Hell,恩,这个地狱还有自己的网站(callbackhell),上面可以找到更多的描述。

ES6的Promise就是对Callback的一种抽象,具体的标准在这里Promise.

一张图足以说明Promise的大致机制: 可以看到,Promise把callback机制的层层嵌套平面化了:

//create a method that return a promise
let doSomethingA = new Promise(
                      (resolve, reject) =>{
                          if(someCase){ resolve("success");}
                          else{ reject("fail");}
                      }
                  );

//chain promise
doSomethingA.then(
                   (successValue) => doSomethingB,
                   (failVale)   => errorHandler)
            .then(...)
            .catch((error) => errorHandler)

Promise的另一个好处是把错误的处理(reject)给抽象出来了,你再也不用在每个callback里面到处写类似if(err){ errorHandler();}的错误处理语句了。

需要注意的是,.then(successHandler,errorHandler)形式中的那两个handler,只能获取到上一个Promise resolve或者reject出来的值。比如上个例子里,成功和失败时收到的值分别为"success""fail"

仔细想想“只能获取上一个Promise的值”代表了什么。如果我有A、B、C三件事情要做,B依赖A的返回值,C依赖A和B的返回值,该怎么做?

A.then((resultA)=>{return B(valueA);})
 .then((resultB)=>{return C(valueA,valueB)}) //ERROR cannot read valueA here
 .catch(...)

解决方案有2个,一是修改方法B,让他的返回值里面包含了他得到的resultA,另外一个方法就是把Promise也嵌套(恩,还是没逃过)。

事实上,基于Single Responsibility Principle,第一种方式是不推荐的。方法B为什么要返回多余的数据给C?

所以,最后这段程序一般会写成这样:

A.then(
    (resultA)=>B(valueA).then(
                               (resultB)=>C(resultA,resultB)
                             )
 .catch(...)

是不是有点烧脑?可以想象下更复杂的逻辑会怎么样!

当然在一些Promise封装库的帮助下,比如Q或者bluebird的帮助下,我们能把代码写的更漂亮,更简单易懂点,恩,也就“好一点”而已。

总结:Promise能在一定程度上平面化Callback hell,抽象的处理Promise中发生的错误


Promise是ES6的标准,事实上,现在大部分主流浏览器都已经支持了标准的Promise写法,下面我们要讲到的async/await是一个ES7的语法。这意味着这种写法需要用工具(比如Babel)编译成ES5/ES6,或者,比如我现在在项目里使用JS的超集Typescript,直接支持这个语法。

async/await比Promise更进一步,能让你把异步的代码写出同步的风格来,就拿前一段的三个方法来说,如果用了async/await,写出来就是这个风格的:(注意下面的代码是typescript的)

"use strict";
async function methodA() {
    return new Promise<string>((resolve)=> {
        //do something
        resolve("valueA");
    });
}

async function methodB(resultA) {
    return new Promise<string>((resolve)=> {
        //do something
        resolve(`valueB from ${resultA}`);
    });
}

async function methodC(resultA, resultB) {
    return new Promise<string>((resolve)=> {
        //do something
        resolve(`${resultA} | ${resultB}`);
    });
}

async function main() {
    let resultA = await methodA();
    let resultB = await methodB(resultA);
    let resultC = await methodC(resultA, resultB);
    console.log(resultC);
}

main();

最后的输出是期望中的"valueA | valueB from valueA",没有回调函数!连then的嵌套都没有了!变量名,调用顺序也更像同步代码了!

你甚至可以在await methodA()外面套上try/catch模块来以同步的方式处理错误.

当然,如果你去看看编译出来的JS的话,会发现async/await其实是使用了generator,在Promise基础上更高级的语法糖而已:

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments)).next());
    });
};

总结:下下一代的JS标准ES7中支持了更高级的语法糖asycn/await,能以类似同步代码的书写方式来写异步代码


总的来说,NodeJS中的异步是需要“转转脑筋”的,特别是对于我这种写了10年JAVA的人来说。所以一般对于NodeJS的适用范围:处理高并发、高I/O,逻辑不复杂,是有一定道理的。当然,搞清楚了异步,用NodeJS写复杂的业务逻辑也并非难事。统一语言,只用一个技能树开发前后端无论从架构、人员培训、质量控制等各个方面来讲还是有很大优势的。