异步编程很好用,但在很多时候也会因为与我们的思维方式不一致而产生一些问题。
1、异常处理的捕获
在介绍这个之前,我们需要了解Node的时间循环机制:
循环机制:Node进程在启动时会创建一个循环,每次循环都会检查是否有待处理事件,若存在就执行相对应的回调函数。每一次循环称为一个Tick。
OK,进入主题。
在PHP或者Java中,我们常会用try、catch、finally等来捕获异常,因此,我么很有可能会写出以下的代码:
try {
someFunction(function(){
// 回调函数的具体实现
});
} catch (e) {
console.log(e.getMessage());
// ...
}
但实际上,在调用someFunction()方法后,回调函数会被保存起来,直至下一个Tick才会被执行,而try/catch只能捕获当前Tick内的异常。
解决方案
Node约定将异常作为回调函数的第一个参数传回,如:
someFunction(function (error, result) {
if (error) {
// 出现异常
} else {
// 正常执行
}
});
如果存在异常,error为具体的异常;如果正常执行,没有异常,error则为null。
2、不能阻塞代码
在JavaScript中,没有提供sleep()线程沉睡功能的函数,只有setInterval()和setTimeout()这两个延时执行的函数,且不会阻塞后续代码的执行。
解决方案
当需要用到阻塞时,考虑用setTimeout去实现😂。
3、多线程编程
Node属于单线程的编程语言,一个Node进程不能充分利用多核CPU的资源,而复杂的业务逻辑往往要求需要很好的利用多核CPU以提供更多的计算服务。
解决方案
当需要充分利用多核CPU时,可以参考Web Worker的实现,即Mater-Worker模型,通过Master创建工作线程,使用子线程去计算,以充分利用CPU资源。
在Node中,我们可以使用Node的child_process的API去进行相关的需求实现。
4、}}}}}嵌套问题
① 无顺序 ((A、B全部完成)->C)
多个操作存在先后依赖关系,执行C需要A、B过程的结果(虽然A、B之间并不存在依赖关系,但C的执行A、B的结果缺一不可)。如图:
graph LR;
A--结果1-->C;
B--结果2-->C;
在同步编程中,我们是这样做的(PHP):
$result1 = A();
$result2 = B();
$result = C($result1, $result2);
但是在JavaScript中是使用异步编程的,通过回调函数来执行后续操作,而且A、B、C三个回调函数的执行并不存在顺序关系,例如:
A(callbackA);
B(callbackB);
C(callbackC);
上述代码中,callbackA、callbackB、callbackC的执行顺序是不固定的,我们不能保证C在A、B之后执行。
那如何实现这个需求呢?
我们可以使用函数嵌套将整个执行过程串行化,将上面的代码调整下:
A(function (result1) {
B(function (result2) {
C(function (result) {
// ...
});
});
});
这样确实可以实现目标需求,但却出现了}}}}}嵌套问题,也称为恶魔金字塔,过多的嵌套让代码显得很难看,不优雅。而且,上述整个过程的执行时间为:
A的耗时 + B的耗时 + C的耗时。
解决方案1
如何更好地处理这个呢?使用变量进行计数:
var count = 0;
var results = {};
var done = function (key, value) {
results[key] = value;
count++;
if (count === 2) { // A, B两次
C(results);
}
}
// 在A、B的回调函数中调用done
A(function (data) {
done('A', data);
});
B(function (data) {
done('B', data);
});
这样就可以避免使用嵌套,同时还能降低耗时,上述代码的总执行时间为:
max(A的耗时, B的耗时) + C的耗时。
优化
上述代码还可以用偏函数、事件发布/订阅模式进行优化。
偏函数:用于生成已预置参数或变量的函数的函数,首先它是一个函数,其作用是生成参数或变量已预置的函数。例如:
var add = function (a, b) { return a + b; } var add_1 = function (num) { // 将add()函数的参数预置为1,得到加1函数 return add(num, 1); } var sub_1 = function (num) { // 将add()函数的参数预置为-1,得到减1函数 return add(num, -1); }
事件发布/订阅模式:在JavaScript中很常见,对应的就是触发事件/监听事件,在Node中的具体实现:
// 订阅 emitter.on("eventName", function (param1, param2) { console.log(param1, param2); // console.log(arguments); // arguments为传入的参数数组 }); // 发布 emitter.emit("eventName", "This is param1", "This is param2");
PHP中获取参数:function test(...$args) {}或使用函数func_get_args、func_get_arg、func_num_args
优化后的代码:
// 偏函数
var after = function (times, callback) {
var count = 0;
var results = {};
return function (key, value) {
results[key] = value;
count++;
if (count === times) {
callback(results);
}
}
}
var done = after(2, C);
emitter.on("done", done); // 添加监听事件
A(function (data) {
emitter.emit("done", "A", data); // 触发done事件,并将结果传回
});
B(function (data) {
emitter.emit("done", "B", data); // 触发done事件,并将结果传回
});
解决方案2
使用EventProxy模块:
const EventProxy = require("eventproxy");
var proxy = new EventProxy();
// 使用all监听event1、event2事件,当两个事件都完成时,调用C回调函数。
proxy.all("event1", "event2", C);
A(function (result1) {
// 触发event1事件
proxy.emit("event1", "This is event1 argument", result1);
});
B(function (result2) {
// 触发event2事件
proxy.emit("event2", "This is event2 argument", result2);
});
EventProxy在每一次触发非all事件时,都会触发一次all事件。
解决方案3
使用Promise(后续介绍)
② 有顺序(A->B->C)
C依赖于B,B依赖于A
graph LR;
A--结果1-->B;
B--结果2-->C;
这种情况也能通过函数嵌套来将整个过程串行化,从而实现该需求,时间消耗为:
A的耗时 + B的耗时 + C的耗时
解决方案
因为这种情况存在先后依赖关系,所以耗时不能进行优化,只能优化代码的函数嵌套问题。
同样可以使用Promise进行优化(后续介绍)