NodeJS学习之异步编程

NodeJS -- 异步编程

 NodeJS最大的卖点--事件机制和异步IO,对开发者并不透明编程

代码设计模式

异步编程有不少特有的代码设计模式,为了实现一样的功能,使用同步方式和异步方式编写代码会有很大差别,如下举例。
一、函数返回值
使用一个函数的输出做为另外一个函数的输入是常见的需求,在同步方式下通常如下述方式编写代码:
var output = fn1(fn2('input'));
// Do something;

在异步方式下,因为函数执行结果不是经过返回值,而是经过回调函数传递,所以通常按如下方式编写代码:设计模式

fn2('input', function(output2) {
    fn1(output2, function(output1) {
        //Do something.
    });
});
:这种方式就是一个函数套一个函数,层级变多了,就很麻烦了
二、遍历数组
在遍历数组时,使用某个函数依次对数据成员作一些处理也是常见的需求,若函数是同步执行,通常如下列方式:
var len = arr.length,
      i = 0;
for(;i < len; ++i) {
    arr[i] = sync(arr[i]);
}
//All array items have processed.

如果异步执行,以上代码就没法保证循环结束后全部数组成员都处理完毕了,若是数组成员必须一个接一个串行处理,则按如下方式编写代码:数组

(function next(i, len, callback) {
    if(i < len) {
        async(arr[i], function(value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
}(0, arr.length, function() {
    // All array items have processed.
}));

能够看到,在异步函数执行一次并返回执行结果后才传入下一个数组成员并开始下一轮执行,直到全部数组成员处理完毕后,经过回调的方式触发后续代码执行。服务器

若数组成员能够并行处理,但后续代码仍然须要全部数组成员处理完毕后才能执行的话,则异步代码调整成如下形式:dom

(function(i, len, count, callback) {
    for(;i < len; ++i) {
        (function(i) {
            async(arr[i], function(value) {
                arr[i] = value;
                if(++count === len) {
                    callback();
                }
            });
        }(i));
    }
}(0, arr.length, 0, function() {
    //All array items have processed.
}));

以上代码并行处理全部成员,并经过计数器变量来判断何时全部数组成员都处理完毕了异步

三、异常处理
JS自身提供的异常捕获和处理机制 -- try...catch...,只能用于同步执行的代码。
但,因为异步函数会打断代码执行路径,异步函数执行过程当中以及执行以后产生的异常 冒泡到执行路径被打断的位置时,若是一直没有遇到try语句,就做为一个全局异常抛出。例:
function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function() {
        callback(fn());
    }, 0);
}

try {
    async(null, function(data) {
        // Do something.
    });
} catch(err) {
    console.log('Error: %s', err.message);
}
-----------------------Console----------------------------
E:\Language\Javascript\NodeJS\try_catch.js:25
        callback(fn());
                 ^

TypeError: fn is not a function
    at null._onTimeout (E:\Language\Javascript\NodeJS\try_catch.js:25:18)
    at Timer.listOnTimeout (timers.js:92:15)

由于代码执行路径被打断了,咱们就须要在异常冒泡到断点以前用try语句把异常捕获注,并经过回调函数传递被捕获的异常,改造:async

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function() {
        try {
            callback(null, fn());
        } catch(err) {
            callback(err);
        }
    }, 0);
}

async(null, function(err, data) {
    if(err) {
        console.log('Error: %s', err.message);
    } else {
        // Do something
    }
})
----------------------Console----------------------
Error: fn is not a function
异常再次被捕获住。
在NodeJS中,几乎全部的异步API都按照以上方式设计,回调函数中第一个参数都是err,所以咱们在编写本身的异步函数时,也能够按照这种方式来处理异常,与NodeJS的设计风格保持一致。
但,咱们的代码一般都是作一些事情,调用一个函数,而后再作一些事情,调用一个函数,若使用同步代码,只须要在入口点写一个try...catch...语句便可捕获全部冒泡的异常,若使用异步代码,那就呵呵了,就像下面,只调用三次异步函数:
function main(callback) {
    // Do something.
    asyncA(function(err, data) {
        if(err) {
            callback(err);
        } else {
            // Do something.
            asyncB(function(err, data) {
                if(err) {
                    callback(err);
                } else {
                    // Do something.
                    asyncC(function(err, data) {
                        if(err) {
                            callback(err);
                        } else {
                            // Do something.
                            callback(null);
                        }
                    });
                }
            });
        }
    });
}
main(function(err) {
    if(err) {
        // Deal with exception.
    }
});

回调函数已经让代码变得复杂了,而异步之下的异常处理更加重了代码的复杂度,幸亏,NodeJS提供了一些解决方案。异步编程

NodeJS提供了domain模块,能够简化异步代码的异常处理。
域,简单讲就是一个JS运行环境,在一个运行环境中,若是一个异常没有被捕获,将做为一个全局异常抛出,NodeJS经过process对象提供了捕获全局异常的方法,例:
process.on('uncaughtException', function(err) {
    console.log('Error: %s', err.message);
});

setTimeout(function(fn) {
    fn();
});
------------------------Console--------------------
Error: fn is not a function
全局异常已被捕获,但大多数异常咱们但愿尽早捕获,并根据结果决定代码执行路径。
用HTTP服务器代码做例子:
function async(req, callback) {
    // Do something.
    asyncA(req, function(err, data) {
        if(err) {
            callback(err);
        } else {
            // Do something.
            asyncB(req, function(err, data) {
                if(err) {
                    callback(err);
                } else {
                    // Do something.
                    asyncC(req, function(err, data) {
                        if(err) {
                            callback(err);
                        } else {
                            // Do something.
                            callback(null, data);
                        }
                    });
                }
            });
        }
    });
}

http.createServer(function(req, res) {
    async(req, function(err, data) {
        if(err) {
            res.writeHead(500);
            res.end();
        } else {
            res.writeHead(200);
            res.end();
        }
    });
});

以上将请求对象交给异步函数处理,再根据处理结果返回响应,这里采用了使用回调函数传递异常的方案,所以async函数内部若再多几个异步函数调用的话,代码就更难看了,为了让代码好看点,能够在没处理一个请求时,使用domain模块建立一个子域,在子域内运行的代码能够随意抛出异常,而这些异常能够经过子域对象的error事件统一捕获,改造:函数

function async(req, callback) {
    // Do something.
    asyncA(req, function(data) {
        // Do something.
        asyncB(req, function(data) {
            // Do something.
            asyncC(req, function(data) {
                // Do something.
                callback(data);
            });
        });
    });
}

http.createServer(function(req, res) {
    var d = domain.create();

    d.on('error', function() {
        res.writeHead(500);
        res.end();
    });

    d.run(function() {
        async(req, function(data) {
            res.writeHead(200);
            res.end(data);
        });
    });
});
注意:
不管是经过process对象的uncaughtException事件捕获到全局异常,仍是经过子域对象的error事件捕获到子域异常,在NodeJS官方文档均强烈建议处理完异常后应当即重启程序,而不是让程序继续运行,由于发生异常后程序处于一个不肯定运行状态,若不退出,可能会发生严重内存泄漏,也可能表现得很奇怪

注:JS的throw...tyr...catch异常处理机制并不会致使内存泄漏和使程序执行出乎意料,而是由于NodeJS并非纯粹的JS,NodeJS里大量的API内部是用C/C++实现的,所以NodeJS程序运行过程当中,代码执行路径穿梭于JS引擎内外部,而JS异常抛出机制可能打断正常代码的执行流程,致使C/C++部分的代码表现异常,进而致使内存泄漏。spa

所以,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,若不能肯定是否会致使内存泄漏等问题,最好在处理完异常后重启程序比较稳当,而使用try语句捕获的异常通常是JS自己的异常,不用担忧上述问题

相关文章
相关标签/搜索