回调

  • 异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了
function heavyCompute(n, callback) {
var count = 0,
i, j; for (i = n; i > 0; --i) {
for (j = n; j > 0; --j) {
count += 1;
}
} callback(count);
} heavyCompute(10000, function (count) {
console.log(count);
}); console.log('hello'); -- Console ------------------------------
100000000
hello
  • 以上代码中的回调函数仍然先于后续代码执行。JS本身是单线程运行的,不可能在一段代码还未结束运行时去运行别的代码,因此也就不存在异步执行的概念。
  • 如果某个函数做的事情是创建一个别的线程或进程,并与JS主线程并行地做一些事情,并在事情做完后通知JS主线程,那情况又不一样了
setTimeout(function () {
console.log('world');
}, 1000); console.log('hello'); -- Console ------------------------------
hello
world
  • 我们可以认为setTimeout这类JS规范之外的由运行环境提供的特殊函数做的事情是创建一个平行线程后立即返回,让JS主进程可以接着执行后续代码,并在收到平行进程的通知后再执行回调函数
  • 除了setTimeoutsetInterval这些常见的,这类函数还包括NodeJS提供的诸如fs.readFile之类的异步API。
  • JS是单线程运行的这个事实上,这决定了JS在执行完一段代码之前无法执行包括回调函数在内的别的代码。也就是说,即使平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程空闲时才能开始执行

代码设计模式

函数返回值

  • 使用一个函数的输出作为另一个函数的输入是很常见的需求,在同步方式下一般按以下方式编写代码:
var output = fn1(fn2('input'));
  • 在异步方式下,由于函数执行结果不是通过返回值,而是通过回调函数传递,因此一般按以下方式编写代码:
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.
})); //以上代码在异步函数执行一次并返回执行结果后才传入下一个数组成员并开始下一轮执行,直到所有数组成员处理完毕后,通过回调的方式触发后续代码的执行。
  • 如果数组成员可以并行处理,但后续代码仍然需要所有数组成员处理完毕后才能执行的话,则异步代码会调整成以下形式:
(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.
}));
  • 与异步串行遍历的版本相比,以上代码并行处理所有数组成员,并通过计数器变量来判断什么时候所有数组成员都处理完毕了。

异常处理

  • S自身提供的异常捕获和处理机制——try..catch..,只能用于同步执行的代码。

  • 异常会沿着代码执行路径一直冒泡,直到遇到第一个try语句时被捕获住。但由于异步函数会打断代码执行路径,异步函数执行过程中以及执行之后产生的异常冒泡到执行路径被打断的位置时,如果一直没有遇到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 ------------------------------
/home/user/test.js:4
callback(fn());
^
TypeError: object is not a function
at null._onTimeout (/home/user/test.js:4:13)
at Timer.listOnTimeout [as ontimeout] (timers.js:110: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: object is not a function
  • 异常再次被捕获住了。在NodeJS中,几乎所有异步API都按照以上方式设计,回调函数中第一个参数都是err。因此我们在编写自己的异步函数时,也可以按照这种方式来处理异常,与NodeJS的设计风格保持一致。

  • 有了异常处理方式后,接着可以想一想一般我们是怎么写代码的。基本上,我们的代码都是做一些事情,然后调用一个函数,然后再做一些事情,然后再调用一个函数,如此循环。如果我们写的是同步代码,只需要在代码入口点写一个try语句就能捕获所有冒泡上来的异常

function main() {
// Do something.
syncA();
// Do something.
syncB();
// Do something.
syncC();
} try {
main();
} catch (err) {
// Deal with exception.
}
  • 如果写的是异步代码,由于每次异步函数调用都会打断代码执行路径,只能通过回调函数来传递异常,于是就需要在每个回调函数里判断是否有异常发生,于是只用三次异步函数调用,就会产生下边这种代码。
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.
}
});

(Domain)

  • NodeJS提供了domain模块,可以简化异步代码的异常处理
  • 简单的讲,一个域就是一个JS运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出。
  • NodeJS通过process对象提供了捕获全局异常的方法
process.on('uncaughtException', function (err) {
console.log('Error: %s', err.message);
}); setTimeout(function (fn) {
fn();
}); -- Console ------------------------------
Error: undefined is not a function
  • 虽然全局异常有个地方可以捕获了,但是对于大多数异常,希望尽早捕获,并根据结果决定代码的执行路径 , 以下HTTP服务器代码作为例子:
function async(request, callback) {
// Do something.
asyncA(request, function (err, data) {
if (err) {
callback(err);
} else {
// Do something
asyncB(request, function (err, data) {
if (err) {
callback(err);
} else {
// Do something
asyncC(request, function (err, data) {
if (err) {
callback(err);
} else {
// Do something
callback(null, data);
}
});
}
});
}
});
} http.createServer(function (request, response) {
async(request, function (err, data) {
if (err) {
response.writeHead(500);
response.end();
} else {
response.writeHead(200);
response.end(data);
}
});
});
  • 以上代码将请求对象交给异步函数处理后,再根据处理结果返回响应。
  • 这里采用了使用回调函数传递异常的方案,因此async函数内部如果再多几个异步函数调用的话,代码就变成上边这副鬼样子了。
  • 为了让代码好看点,可以在每处理一个请求时,使用domain模块创建一个子域(JS子运行环境)。
  • 在子域内运行的代码可以随意抛出异常,而这些异常可以通过子域对象的error事件统一捕获。于是以上代码可以做如下改造:
function async(request, callback) {
// Do something.
asyncA(request, function (data) {
// Do something
asyncB(request, function (data) {
// Do something
asyncC(request, function (data) {
// Do something
callback(data);
});
});
});
} http.createServer(function (request, response) {
var d = domain.create(); d.on('error', function () {
response.writeHead(500);
response.end();
}); d.run(function () {
async(request, function (data) {
response.writeHead(200);
response.end(data);
});
});
});
  • 使用.create方法创建了一个子域对象,并通过.run方法进入需要在子域中运行的代码的入口点。而位于子域中的异步函数回调函数由于不再需要捕获异常

陷阱

  • 无论是通过process对象的uncaughtException事件捕获到全局异常,还是通过子域对象的error事件捕获到了子域异常,在NodeJS官方文档里都强烈建议处理完异常后立即重启程序,而不是让程序继续运行。
  • 按照官方文档的说法,发生异常后的程序处于一个不确定的运行状态,如果不立即退出的话,程序可能会发生严重内存泄漏,也可能表现得很奇怪。
  • 这里需要澄清一些事实。JS本身的throw..try..catch异常处理机制并不会导致内存泄漏,也不会让程序的执行结果出乎意料,但NodeJS并不是存粹的JS。NodeJS里大量的API内部是用C/C++实现的,因此NodeJS程序的运行过程中,代码执行路径穿梭于JS引擎内部和外部,而JS的异常抛出机制可能会打断正常的代码执行流程,导致C/C++部分的代码表现异常,进而导致内存泄漏等问题。
  • 因此,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,如果不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当。而使用try语句捕获异常时一般捕获到的都是JS本身的异常,不用担心上诉问题。

node.js整理 06异步编程的更多相关文章

  1. 17.Node.js 回调函数--异步编程

    转自:http://www.runoob.com/nodejs/nodejs-tutorial.html Node.js 异步编程的直接体现就是回调. 异步编程依托于回调来实现,但不能说使用了回调后程 ...

  2. 使用node.js 进行服务器端JavaScript编程

            node.js 入门        node.js 可以运行在 Linux.Windows 和 Macintosh 等主流的操作系统上.在 Windows 平台上运行 node.js ...

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

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

  4. Node.js入门:异步IO

    异步IO     在操作系统中,程序运行的空间分为内核空间和用户空间.我们常常提起的异步I/O,其实质是用户空间中的程序不用依赖内核空间中的I/O操作实际完成,即可进行后续任务. 同步IO的并行模式 ...

  5. promise 的基本概念 和如何解决js中的异步编程问题 对 promis 的 then all ctch 的分析 和 await async 的理解

    * promise承诺 * 解决js中异步编程的问题 * * 异步-同步 * 阻塞-无阻塞 * * 同步和异步的区别? 异步;同步 指的是被请求者 解析:被请求者(该事情的处理者)在处理完事情的时候的 ...

  6. Node.js中的异步I/O是如何进行的?

    Node.js的异步I/O通过事件循环的方式实现.其中异步I/O又分磁盘I/O和网络I/O.在磁盘I/O的调用中,当发起异步调用后,会将异步操作送进libuv提供的队列中,然后返回.当磁盘I/O执行完 ...

  7. 【第三周读书笔记】浅谈node.js中的异步回调和用js-xlsx操作Excel表格

    在初步学习了node.js之后,我发现他的时序问题我一直都很模糊不清,所以我专门学习了一下这一块. 首先我们来形象地理解一下进程和线程: 进程:CPU执行任务的模块.线程:模块中的最小单元. 例如:c ...

  8. Node.js 教程 06 - 函数

    前言: 本篇介绍的是Node.js中的函数,相对于上一篇会简单一点,其实和我们Javascript中的function无异. 好了,废话不多说了,我们进入正题吧. Node.js函数: [示例1:创建 ...

  9. 【JS】370- 总结异步编程的六种方式

    点击上方"前端自习课"关注,学习起来~ 作者:Aima https://segmentfault.com/a/1190000019188824 众所周知 JavaScript 是  ...

随机推荐

  1. tomcat URL乱码问题

    用get传参时,显示乱码 在tomcat里的server.xml中添加一下即可. <Connector port="8080" protocol="HTTP/1.1 ...

  2. 【编程之美】2.5 寻找最大的k个数

    有若干个互不相等的无序的数,怎么选出其中最大的k个数. 我自己的方案:因为学过找第k大数的O(N)算法,所以第一反应就是找第K大的数.然后把所有大于等于第k大的数取出来. 写这个知道算法的代码都花了2 ...

  3. 不同版本CUDA编程的问题

    1 无法装上CUDA的toolkit 卸载所有的NVIDIA相关的app,包括NVIDIA的显卡驱动,然后重装. 2之前的文件打不开,one or more projects in the solut ...

  4. poj 3461Oulipo

    题目链接:http://poj.org/problem?id=3461 统计字符串出现的次数 #include<cstdio> #include<iostream> #incl ...

  5. python 中内存映射二进制文件

    内存映射一个文件并不会导致整个文件被读取到内存中. 也就是说,文件并没有被复制到内存缓存或数组中.相反,操作系统仅仅为文件内容保留了一段虚拟内存. 当你访问文件的不同区域时,这些区域的内容才根据需要被 ...

  6. HTTP 请求头中的 X-Forwarded-For

    https://imququ.com/post/x-forwarded-for-header-in-http.html

  7. Delphi面向对象的属性

    可以把属性看成是能对类中的数据进行修改和执行代码的特殊的辅助域.对于组件来说,属性就是列在Object Inspector窗口的内容.下面的例子定义了一个有属性的简单对象 TMyObject = cl ...

  8. Sexagenary Cycle(天干地支法表示农历年份)

    Sexagenary Cycle Time Limit: 2 Seconds      Memory Limit: 65536 KB 题目链接:zoj 4669 The Chinese sexagen ...

  9. python类中的super,原理如何?MRO是什么东东?

    下面这个URL解释得比较清楚. http://python.jobbole.com/86787/?utm_source=group.jobbole.com&utm_medium=related ...

  10. forEach 方法 (Array) (JavaScript)

    为数组中的每个元素执行指定操作. 语法 array1.forEach(callbackfn[, thisArg]) 参数 参数 定义 array1 必选.一个数组对象. callbackfn 必选.最 ...