NodeJS -- 异步编程

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

代码设计模式

异步编程有很多特有的代码设计模式,为了实现同样的功能,使用同步方式和异步方式编写代码会有很大差异,以下举例。
1、函数返回值
使用一个函数的输出作为另一个函数的输入是常见的需求,在同步方式下一般以下述方式编写代码:

var output = fn1(fn2('input'));
// Do something;

在异步方式下,由于函数执行结果不是通过返回值,而是通过回调函数传递,因此一般按以下方式编写代码:

fn2('input', function(output2) {
fn1(output2, function(output1) {
//Do something.
});
});
注:这种方式就是一个函数套一个函数,层级变多了,就很麻烦了
2、遍历数组
在遍历数组时,使用某个函数依次对数据成员做一些处理也是常见的需求,若函数是同步执行,一般以下列方式:

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.
}));

可以看到,在异步函数执行一次并返回执行结果后才传入下一个数组成员并开始下一轮执行,直到所有数组成员处理完毕后,通过回调的方式触发后续代码执行。

若数组成员可以并行处理,但后续代码仍然需要所有数组成员处理完毕后才能执行的话,则异步代码调整成以下形式:

(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.
}));

以上代码并行处理所有成员,并通过计数器变量来判断什么时候所有数组成员都处理完毕了

3、异常处理
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语句把异常捕获注,并通过回调函数传递被捕获的异常,改造:

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++部分的代码表现异常,进而导致内存泄漏。

因此,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,若不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当,而使用try语句捕获的异常一般是JS本身的异常,不用担心上述问题

NodeJS学习之异步编程的更多相关文章

  1. nodejs学习笔记 —— 异步编程解决方案

    在js或者node编程中,由于异步的频繁和广度使用,使得回调和嵌套的深度导致编程的体验遇到一些挑战,如果写出优雅和好看的代码,本文主要针对异步编程的主流方案做一些总结 1.事件发布/订阅模式 事件监听 ...

  2. 深入理解nodejs中的异步编程

    目录 简介 同步异步和阻塞非阻塞 javascript中的回调 回调函数的错误处理 回调地狱 ES6中的Promise 什么是Promise Promise的特点 Promise的优点 Promise ...

  3. nodejs之async异步编程

    1.什么是异步编程? 异步编程是指由于异步I/O等因素,无法同步获得执行结果时, 在回调函数中进行下一步操作的代码编写风格,常见的如setTimeout函数.ajax请求等等. 示例:  for (v ...

  4. 《C#并发编程经典实例》学习笔记—异步编程关键字 Async和Await

    C# 5.0 推出async和await,最早是.NET Framework 4.5引入,可以在Visual Studio 2012使用.在此之前的异步编程实现难度较高,async使异步编程的实现变得 ...

  5. 09-Node.js学习笔记-异步编程

    同步API,异步API 同步API:只有当前API执行完成后,才能继续执行下一个API console.log('before'); console.log('after'); 异步API:当前API ...

  6. Java8学习之异步编程

    异步编程 所谓异步其实就是实现一个无需等待被调用函数的返回值而让操作继续运行的方法 创建任务并执行任务 无参创建 CompletableFuture<String> noArgsFutur ...

  7. 学习Promise异步编程

    JavaScript引擎建立在单线程事件循环的概念上.单线程( Single-threaded )意味着同一时刻只能执行一段代码.所以引擎无须留意那些"可能"运行的代码.代码会被放 ...

  8. 借助Q.js学习javascript异步编程。

    金字塔式 //add1 function step1(n, callback) { setTimeout(function(){ callback.call(null, n + 1); }, 100) ...

  9. nodejs 学习三 异步和同步

    同步函数 for (let i = 0; i < 10; i ++) { setTimeout(() => { console.log(`${i} ______ ${new Date}`) ...

随机推荐

  1. Adding Multithreading Capability to Your Java Applications

    src: http://www.informit.com/articles/article.aspx?p=26326&seqNum=3 Interrupting Threads A threa ...

  2. js面向对象,有利于复用

    需求:在网页上添加个天气预报. 以前总是在需要执行js的地方,直接写function(){}.在需要同样功能的地方直接copy,或者稍微修改. 然后在网上看看有没有好点的方法,然后就看到js面向对象编 ...

  3. 几种连接数据库的OLEDB驱动程序

    以下是连接几种数据库的驱动,把用"<%"和"%>"括住的地方保存为文件你就可以直接调用了 连接Access数据库 <% dim conn,db ...

  4. Codeforces Gym 100015C City Driving 离线LCA

    City Driving 题目连接: http://codeforces.com/gym/100015/attachments Description You recently started fre ...

  5. hdu 5258 数长方形 离散化

    数长方形 Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=5258 Des ...

  6. js中日期转换为时间戳

    function dateToTimestamp(date) { //方法一 var newDate = new Date(); newDate.setFullYear(date.substring( ...

  7. Servlet 3.0 新特性

    Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布.该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发 ...

  8. History(历史)命令用法15例

    导读 如果你经常使用 Linux 命令行,那么使用 history(历史)命令可以有效地提升你的效率,本文将通过实例的方式向你介绍 history 命令的 15 个用法. 使用 HISTTIMEFOR ...

  9. Linux编程之《只运行一个实例》

    概述 有些时候,我们要求一个程序在系统中只能启动一个实例.比如,Windows自带的播放软件Windows Medea Player在Windows里就只能启动一个实例.原因很简单,如果同时启动几个实 ...

  10. LeetCode39 Combination Sum

    题目: Given a set of candidate numbers (C) and a target number (T), find all unique combinations in C  ...