与善人居,如入芝兰之室,久而不闻其香,即与之化矣.与不善人居,如入鲍鱼之肆,久而不闻其臭,亦与之化矣。丹之所藏者赤,漆之所藏者黑,是以君子必慎其所处者焉。 —— 《孔子家语》

npm常用命令

1
2
3
4
# 查看全局安装的node模块的位置
npm root -g
# 消除 mac 下全局安装模块报错 permission deny
sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}

多进程

cluster模块,可以把任务分配给子进程,就是说Node把当前程序复制了一份给另一个进程每个子进程有些特殊的能力,比如能够与其他子进程共享socket连接(多个进程共享socket连接,那么端口不就冲突了么?)。这样我们就可以写一个Node程序,让它创建许多其他Node程序,并把任务分配给它们。需要重点理解的是,你用cluster把工作共享到一组复制的Node程序时,主进程不会参与到每个具体的事务中。主进程管理所有的子进程,但当子进程与I/O操作交互时,它们是直接进行操作的,不需要通过主进程。这意味着,如果你用cluster来创建一个Web服务器,请求将不会通过你的主进程,而是直接连接到子进程。而且,调度这些请求并不会导致系统出现瓶颈。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length; // 获取CPU内核数

var count = 0;

// 是则根据CPU内核数创建worker进程
if (cluster.isMaster) {
// Fork workers
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}

Object.keys(cluster.workers).forEach(function(id) {
console.log('I am worker running with ID : ' + cluster.workers[id].process.pid);
});
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
// restart a worker
cluster.fork();
});
} else {
console.log(count++); // 每次打印都是0
// 那么此worker进程就启动一个http服务
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
}

cluster提供了跨平台时让多个进程共享socket的方法。即使多个子进程在共享一个端口上的连接,其中一个堵塞了,也不会影响其他工作进程的新连接。(PS:这到底是如何做到的?)
process.strerr是阻塞的可写流,编码永远是UTF8.生产环境中应该避免向其写入过多内容.

事件监听

如何实现EventEmitter

我们来分析下EventEmitter的简略实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
EventEmitter.prototype.emit = function(type){
// ...
var handler = this._events[type]
// ...
} else if(isArray(handler)){
var args = Array.prototype.slice.call(arguments,1)
var listeners = handler.slice();
for(var i = 0,l = listeners.length;i < l;i++){
listeners[i].apply(this,args)
}
return true
}
// ...
}

在事件触发后,运行时处理程序中的一项检查是看看是否存在事件监听器的数组。如果有几个监听器,运行执行器会按数组顺序把里面的监听器一一调用。意思是说,第一个绑定的监听器会首先用apply()方法调用,然后是第二个,以此类推。这里需要重点注意的是,一个事件的所有监听器是在同一个代码路径上的。所以如果其中一个回调函数出现了异常未被捕获,将导致该事件的其他回调函数终止执行。

使用EventEmitter监听HTTP会话

httpsniffer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
'use strict'

const url = require('url')
const util = require('util')

exports.sniffOn = server => {

let _reqToString = req => {
let ret = `[request:]${req.method} ${req.httpVersion} ${req.url}\n${JSON.stringify(url.parse(req.url,true))}\n`
let keys = Object.keys(req.headers)
for(let i = 0;i < keys.length;i++){
let key = keys[i]
ret += `${i} ${key}:${req.headers[key]}\n`
}
if(req.trailers){
ret += `${req.trailers}\n`
return ret
}
}

server.on('request',(req,res) => {
util.log('========= e_request =========')
util.log(_reqToString(req))
})
server.on('close',errno => {
util.log('[e_close] errno = ',errno)
})
server.on('checkContinue',(req,res) => {
util.log('e_checkContinue')
util.log(_reqToString(req))
res.writeContinue()
})
server.on('upgrade',(req,socket,head) => {
util.log('e_upgrade')
util.log(_reqToString(req))
})
server.on('clientError',() => {
util.log('e_clientError')
})
}

test_httpsniffer.js

1
2
3
4
5
6
7
8
9
10
11
12
'use strict'

const http = require('http')
const sniffer = require('./httpsniffer')

let server = http.createServer((req,res) => {
res.writeHead(200,{'Content-Type':'text/plain'})
res.end('hello world\n')
})

sniffer.sniffOn(server)
server.listen(3000)

EventEmitter中newListenser事件的妙用

newListener可以用来做事件机制的反射,特殊应用,事件管理等。当任何on事件添加到EventEmitter时,就会触发newListener事件,基于这种模式,我们可以做很多自定义处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const EventEmitter = require('events');

class MyEmitter extends EventEmitter {
}

var emitter = new MyEmitter();
emitter.on('newListener', function (name, listener) {
console.log("添加事件:", name);
console.log("新事件的处理函数:", listener);
setTimeout(function () {
console.log("我是自定义延时处理机制");
}, 1000);
});
emitter.on('hello', function () {
console.log('hello');
});
emitter.on('world', function () {
console.log('world');
});

输出结果

1
2
3
4
5
6
7
8
9
10
添加事件: hello
新事件的处理函数: function hello() {
console.log('hello');
}
添加事件: world
新事件的处理函数: function world() {
console.log('world');
}
我是自定义延时处理机制
我是自定义延时处理机制

异步流程控制

模仿async库的waterfall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'use strict'

/**
* sync_exec.js
* 使函数队列顺序执行,参考async.js@waterfall
*
* @param {[Array]} fnQ [函数队列]
* @param {[type]} args [每个函数的共同参数] [optional]
* @return {[Undefined]} [Undefined]
*/
let syncExec = module.exports = function(fnQ,args){

/**
* 递归函数,依次执行函数队列中的函数
* @param {[Array]} fnQ [函数队列]
* @param {[Number]} count [函数执行计数器]
* @param {[Number]} total [队列长度]
* @param {[Array]} argv [每个函数的共有参数]
* @param {[object]} self [函数绑定的this指向]
* @return {[Undefined]} [undefined]
*/
let _exec = function(fnQ,count,total,argv,self){
if(count === total) return
fnQ[count].call(self,
function(){
_exec(fnQ,++count,total,argv,self)
},
argv
)
}

let self = this
let fLen = fnQ.length
let argv = [].slice.call(arguments,1) // 0是next函数,从1开始是函数真正的参数
return _exec(fnQ,0,fLen,argv,self,void 0)
}

模仿async.parallel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function parallel(arr, cb) {

const len = arr.length;

var results = [];
var error = false;

for (let i = 0; i < len; i++) {
let parr = arr[i].params.concat();
parr.push(function (err, data) {
if (err && !error) {
error = true;
cb(err, results);
} else {
results.push(data);
if (results.length === len) {
cb(null, results);
}
}
});

arr[i].func.apply(arr[i].thisObj, parr);
}
}

function sleep(sec, cb) {
setTimeout(() => {
require('util').log('delay sec ' + sec);
var err = Math.random() > .5 ? null : new Error('error' + sec);
cb(err, sec);
}, sec * 1000);
}

var funcs = [1, 2, 3].map(x => ({func: sleep, params: [x]}));

parallel(funcs, function (err, data) {
console.log('parallel end', err, 'data', data);
});

输出:

1
2
3
4
5
6
7
10 Sep 16:48:33 - delay sec 1
parallel end Error: error1
at Timeout._onTimeout (/Users/yiihua-013/WebstormProjects/hiyundong/test.js:65:43)
at listOnTimeout (internal/timers.js:531:17)
at processTimers (internal/timers.js:475:7) data []
10 Sep 16:48:34 - delay sec 2
10 Sep 16:48:35 - delay sec 3

实现co.js,效果同bluebirdde#coroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function co(gen) {

var hander_error_ = [];

function flow() {
var iter_ = gen();

var next_ = data => {
// http://es6.ruanyifeng.com/#docs/generator#next-%E6%96%B9%E6%B3%95%E7%9A%84%E5%8F%82%E6%95%B0
// 将结果作为参数传递给next,yield左侧的变量就能被正确赋值,同时执行下一个异步操作。采用这种方式从上到下直到这个Generator对象中被yield分割的每一部分都执行完毕
var result = iter_.next(data);
if (!result.done) {
result.value.then(data => {
next_(data);
}).catch(function (err) {
hander_error_.forEach(handler => {
if (typeof handler === 'function') {
handler(err);
}
});
});
}
};

process.nextTick(() => {
try {
next_();
} catch (err) {
hander_error_.forEach(handler => {
if (typeof handler === 'function') {
handler(err);
}
});
}
});
return flow;
}

Object.defineProperty(flow, 'catch', {
enumerable: false,
value : function (handler) {
if (typeof handler === 'function') {
hander_error_.push(handler);
}
return flow;
}
});

return flow;
}

const readFileAsync = path => new Promise((resolve, reject) => require('fs').readFile(path, (err, data) => err ? reject(err) : resolve(data)));
const delayAsync = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const log = require('util').log;

let hco = co(function *() {
log('co begin');
yield delayAsync(3);
let ret = yield readFileAsync(__filename);
log(ret);
throw new Error('there is an error!');
log('co end');
});
hco().catch(err => console.error('err:', err));

co解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const readFileAsync = path => new Promise((resolve, reject) => require('fs').readFile(path, (err, data) => err ? reject(err) : resolve(data)));
var gen = function* (){
var f1 = yield readFileAsync('/etc/hosts');
console.log('f1',f1.toString());
var f2 = yield readFileAsync('/etc/shells');
console.log('f2',f2.toString());
};

var g = gen();
const step1 = g.next();
log('step1',step1); // step1 { value: Promise { <pending> }, done: false }
step1.value.then(data => {
log('data1',data); // data1 <Buffer 23 23 ... >
// 当promise#onFullfield的时候向next传递参数,yield得到的值就是next传递的参数
let step2 = g.next(data); // 此处的参数影响 gen函数中的第一个console
log('step2',step2); // step2 { value: Promise { <pending> }, done: false }
step2.value.then(data => {
log('data2',data); // data2 <Buffer 23 20 ... >
g.next(data); // 指定gen中的第二个console
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// test_async_exec.js
'use strict'

function App(){}

App.prototype.syncExec = require('./sync_exec')

var app = new App()

app.syncExec([function(next,args){
console.log(1)
console.log('args:',args)
next() // 必须调用next
},function(next,args){
console.log(2)
console.log('args:',args)
next(22)
},function(next,args){
console.log(3)
console.log('args:', args)
next()
},function(next){
console.log(4)
}],{a:-1,b:-2},-3,-4)

// 1
// args: [ { a: -1, b: -2 }, -3, -4 ]
// 2
// args: [ { a: -1, b: -2 }, -3, -4 ]
// 3
// args: [ { a: -1, b: -2 }, -3, -4 ]
// 4

lodash中部分API的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'use strict'

let _ = module.exports = {

extend:function(origin,add){
if(!add || !this.isObject(add)) return origin
let keys = Object.keys(add)
let i = keys.length
while(i--) origin[keys[i]] = add[keys[i]]
return origin
},
isObject:function(obj){
return typeof obj === 'object' && obj !== null
},
isString:function(obj){
return Object.prototype.toStirng.call(obj) === '[object String]'
}
}

参见:coding.net

console.log前面增加时间戳信息

1
2
3
4
5
6
7
8
9
'use strict'

exports.log = function(){
let _log = console.log
let args = Array.from(arguments)
let dateTimeStr = `[ ${Date()} ]`// 日期字符串可采用必要的格式化,可参见moment.js
if('string' === typeof args[0]) dateTimeStr += args.shift(args)
return _log.apply(console,[dateTimeStr].concat(args))
}

参见coding.net

深入理解异步IO

在os中,程序的运行空间分为内核空间和用户空间。我们常常提到的异步IO实际上是用户空间中的程序不用依赖内核空间中的IO操作实际完成,即可进行后续的任务

循环依赖

两个文件互相require并不会造成依赖死循环。第一次被require的对象将是不完整的对象。

require源码解读
循环依赖

一些技巧和优化

  • 在一个函数的内部可以使用console.trace()打印出该函数的调用堆栈,注意:最先调用的方法在底。
  • 少于600个字符的函数v8会进行优化,详见:node优化的小小黑科技
  • 由于utf-8编码是变长编码,长度可能在1~4个字节,所以最佳实践是定义缓冲区的大小为4的整数倍。

ES5和ES6中的实现class

ES5中通常这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
function Agent(options) {

console.log(this);

if (!(this instanceof Agent)) {
return new Agent(options);
}

this.options = options;
}

new Agent({a:1});

ES5中为什么要做instance of的判断呢?这是因为ES5中类都是函数模拟的,所以调用Agent可以不用new关键字,直接像普通函数(或者使用call,apply)一样调用Agent(options),此时的this就是函数调用时的this,但是这并不是我们希望的this,这样在this上的赋值就会污染外部的作用域(这是一个极度危险的操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Agent(options) {
this.options = options;
}

function Agent2(options) {
if (!(this instanceof Agent2)) {
return new Agent2(options);
}
this.options = options;
}

const ctx = {options: 1};

let ret = Agent.call(ctx, -1);
console.log(ctx, ret); // 不使用new得到的实例为undefined,并且ctx中的值被改变了
ret = Agent2.call(ctx, 0);
console.log(ctx, ret); // 同样的不合法的操作可以保证得到正确的实例,并且不会污染外边的作用域中的变量

// { options: -1 } undefined
// { options: -1 } Agent2 { options: 0 }

ES6中的class因为必须使用new关键字,因而简化了代码,无需使用上面的判断。

Socket IO,是完全异步的,没有涉及到线程;而文件IO是用多线程模拟的,具有不同的特点:

  • 本地磁盘读写比网络请求快得多
  • 磁盘文件按块访问,OS的缓存机制使得顺序读写文件的效率极高

Linux下node在启动时会维护一个线程池,libuv的默认线程池大小为4,可以使用UV_THREADPOOL_SIZE,命令行参数启动时指定。线程池使用的地方:

  • fs api
  • 异步加密方法,例如:crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes()
  • dns.lookup()
  • zlib api

因为libuv的线程池是固定大小的,因此如果上述api花费了太长时间,必然会导致后续的上述api调用的性能出现问题。所以增大线程池大小是一个可能会提高程序性能。详见dns.lookup阻塞了fs io

js代码的编译和优化

隐藏类

在静态语言中,开发者定义的类,每一个成员变量都有一个确定的类型。因为有了类型信息,一个对象包含哪些成员和这些成员在对象中的偏移量等信息编译阶段就可以确定,执行时CPU只需要用对象首地址加上成员在对象内部的偏移量就可以访问内部成员,这些访问指令在编译阶段就生成了。但对于js这种动态语言,变量在运行时可以随时由不同的类型赋值,并且对象本身可以随时添加删除成员。访问对象属性需要的信息完全由运行时决定。为了按照索引的方式访问成员,V8“悄悄滴”对运行中的对象分了类,这个过程中产生了一种V8内部的数据结构称为隐藏类,其本身是一个对象。

当定义一个构造函数,使用这个函数生成第一个对象的时候V8会为他初始化一个隐藏类。以后使用这个构造器生成的对象指向同一个隐藏类。但是如果程序中某个对象添加或者删除了某个属性,V8立即创建一个新的隐藏类,改变之后的对象指向新创建的隐藏类。隐藏类起到了分组对象的作用。同一组的对象具有相同的成员名称。隐藏类记录了成员的名称和偏移量,根据这些信息,V8能够按照对象首地址+偏移量访问成员变量。因此创建对象的时候最好一次性指定好对象的成员,动态增添属性会造成隐藏类的派生。

优化回退

程序在运行时,V8会采集js的运行数据,当发现某些函数执行比较频繁,就将其标记为热点函数。针对热点函数,V8会认为此函数比较稳定,类型已经确定,会调用编译器生成更高效的字节码,万一遇到类型变化(例如函数的入参由number变为string),v8会将函数回退到优化前一般的情况。因此针对不同类型的参数尽量多写个函数而不是在同一个函数中进行类型区分。

模板引擎Nunjucks中的坑

后端采用eggjs渲染模板

1
2
3
4
5
6
7
const data = [{"lng":114.0512961,"lat":22.1333079,"count":17},{"lng":114.0522961,"lat":22.2333079,"count":15},{"lng":114.0532961,"lat":22.3333079,"count":12}];
await ctx.render('map', { data });

// map.nj
<script>
var points = {{ data }}
</script>

渲染到页面全部成了[[object Object],[object Object],[object Object]],原来默认会调用toString方法,JSON中的双引号被转义了,所以还需要使用safe防止其转义。

1
2
// var points = {{ data | dump }}  // 这个是错误的,要写成下面的
var points = {{ data | dump | safe }};

参见:(https://github.com/mozilla/nunjucks/issues/94)

生成器函数

生成器函数和Iterator接口

1
2
3
4
5
const obj = {};
obj[Symbol.iterator] = function* () {
for(let i = 0;i < 3;i++) yield i;
}
for(let v of obj) console.log(v)

实现状态机

1
2
3
4
5
6
7
8
9
const state_machine = function* (){
while(true) {
yield 'state:A'
yield 'state:B'
yield 'state:C'
}
}
const s = state_machine();
for(let i = 0;i < 7;i++) console.log(s.next());

装饰器

是一个函数用来修饰类的行为。第三方库code-decorator。日志系统可以用这种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const log = type => {
return function(target,name,descriptor) {
let src_method = descriptor.value;
descriptor.value = (...args) => {
src_method.apply(target,args);
console.log(`log ${type}`);
}
}
}

// 类中只写业务,不埋点
class AD{
@log('show')
show(){
console.log('ad is show');
}
@log('click')
click(){
console.log('ad is click');
}
}

const ad = new AD();
ad.show();
ad.click();

在没有重复使用3次以上的场景下避免使用抽象。过早优化是万恶之源。

async & await 和内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}

const _ = require('lodash');

async function doTask() {
// console.log('start');
await sleep(_.random(10, 30));
// console.log('done');
}

async function start() {
await sleep(3);
await doTask();
await start(); // await 这里存在内存泄漏(将前面的await去掉/改成循环 则不会有这个问题)
}

start();

setInterval(() => {
console.log(process.memoryUsage());
},10e3);

上面的代码存在内存泄漏问题:

进程启动的时候占用内存是22M

1
2
3
4
5
6
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
30211 devel 20 0 1229m 381m 10m S 1.0 9.6 1:36.63 node test.js
# 在start函数的递归调用中取消await
21729 devel 20 0 867m 23m 10m S 1.0 0.6 1:22.73 node test.js
# 改成循环
14498 devel 20 0 867m 24m 10m S 1.0 0.6 1:11.04 node test.js

上面的内存泄漏的原因在于promise不断堆积来不及销毁,参见:https://segmentfault.com/q/1010000017498684?_ea=5678155

Node的Buffer模块性能相关部分有C++实现,所以Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存申请的(Buffer的内存虽然由Node的C/C++层面实现内存申请,但是变量的回收的还是由V8的GC管理的)。