我们都知道关于 NodeJS 中的一个常识是 NodeJS 是一个单线程的 JS 引擎,这是由于使用的 Chrome 的 V8 引擎造成的,而 V8 引擎为何这么设计是有多方面的原因的:
- 防止网站执行多个脚本影响用户体验(你用的多了,别人就没法用了)
- 防止多个进程操作同一个 DOM,发生资源竞争的情况
- JS 的设计一开始就是个玩具...
而为了弥补这种缺陷,使之能执行异步代码,因此 NodeJS 使用了一种事件循环的方式,此处可以看一下我之前写的JavaScript 事件循环(EventLoop)这篇文章,解释的很详细了。
Hack
在 ES5,ES6 的时候,NodeJS 执行异步代码还得需要使用第三方的库进行支持,比如之前的前端大神tj所写的co流程控制器,它使用了 JS 中的生成器来执行异步代码,不会阻塞 NodeJS:
var co = require('co');
var thunkify = require('thunkify');
var request = require('request');
var get = thunkify(request.get);
co(function*() {
var a = yield get('http://google.com');
var b = yield get('http://yahoo.com');
var c = yield get('http://cloudup.com');
console.log(a[0].statusCode);
console.log(b[0].statusCode);
console.log(c[0].statusCode);
})();
v4 之后支持了 Promise,为的是给async/await
让路
var co = require('co');
co(function*() {
// resolve multiple promises in parallel
var a = yield Promise.resolve(1);
var b = yield Promise.resolve(2);
var c = yield Promise.resolve(3);
console.log([a, b, c]);
// => [1, 2, 3]
}).catch(onerror);
上面的代码主要的作用是顺序执行a,b,c
的异步,如果不使用此类库,你就可能会掉入回调地狱里去了:
runA(function() {
runB(function() {
runC();
});
});
如果你的这些a,b,c
没有依赖顺序,你可以并行执行他们,co
也是支持的:
co(function*() {
var a = size('package.json');
var b = size('Readme.md');
var c = size('Makefile');
return yield [a, b, c];
})();
// v4之后
co(function*() {
// resolve multiple promises in parallel
var a = Promise.resolve(1);
var b = Promise.resolve(2);
var c = Promise.resolve(3);
var res = yield [a, b, c];
console.log(res);
// => [1, 2, 3]
}).catch(onerror);
但是后面随着 NodeJS 增加了Promise
以及async/await
语法糖后,这些库就被慢慢的冷藏了,一直没有再更新过。
Async
历史回顾完了,要回答 title 提的问题,我们需要首先知道如何写出一个异步代码,前提只能使用Promise
:
runA().then(() => {
runB().then(() => {
runC().then(() => {
console.log('finish');
});
});
});
可以看出来即使是使用了Promise
,写出来的代码还是会陷入到类似回调地狱的嵌套里面去,因此co
库在Promise
推出来后热度并没有降低多少,直到async/await
的出现:
async function task() {
await runA();
await runB();
await runC();
}
可以看到借鉴了.Net
的异步关键字后写法一下子清晰明了起来,但是你就会开始疑惑了,如果我在 NodeJS 中使用 async 会阻塞我的服务器吗?比如:
const express = require('express');
const app = new express();
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
app.get('/', async (req, res) => {
await sleep(4000);
res.send('sleep done');
});
由于 NodeJS 是单线程的,因此你会担心上面的代码引起服务器的阻塞,导致后一个请求需要等待前一个请求成功后才会响应。
其实并不会的,因为本质上async/await
就是一个Promise
,因此上面的代码其实可以转化为:
const express = require('express');
const app = new express();
app.get('/', (req, res) => {
sleep(4000).then(() => {
res.send('sleep done');
});
});
这样就一眼能看懂了,那么什么情况下会阻塞 NodeJS 的进程呢?
for
,while
这种循环- JSON 的序列号与反序列化,比如
JSON.stringify
,JSON.parse
方法 - 没优化过的正则表达式,比如一些
'aaaaaaaaaab'.match(/^(a|a)+$/)
,这种正则表达式会导致匹配回溯,非常的消耗 CPU,导致 NodeJS 被阻塞(类似 ReDos 攻击)
那不会被阻塞的操作有哪些呢?
- 数据库请求
- 网络请求
- 文件系统访问
所以结论就是需要根据你的代码逻辑来进行判断,没有放之四海而皆准的答案。