Nodejs进阶学习

javascript里程碑事件

1995年javascript诞生
1997年6月,ECMAScript 1.0
1998年6月,ECMAScript 2.0
1999年,ActiveX问世,各大厂商实现自己XMLHttpRequest
1999年12月,ECMAScript 3.0
2005年2月,ajax问世
2006年1月,jquery问世
2008年12月,谷歌浏览器问世(V8)
2009年6月,nodejs问世
2009年12月, ECMAScript 5.0
2010年,npm问世
2015年6月,ECMAScript 6.0
2010年,npm问世

写法规范

es6规范

1
2
3
4
5
6
7
8
// es6规范
import { add } from './tools.js';

// tools.js
module.export = {
add
}

commonjs规范

1
2
3
4
5
// commonjs规范 nodejs(模块缓存)
const { minus } = require('./tools.js');

// tools.js
exports.minus = function(a, b) {return a - b;}

异步解决方案-Promise(或者aysnc/await)

1
2
3
4
5
6
7
8
9
10
11
12
13
const myPromise = new Promise((resolve, reject)=> {
setTimeout(()=> {
resolve('success');
})
// reject('fail');
})

myPromise.then((data)=> {
console.log(data); // resolve的结果 'success'
}, (result)=> {
console.log(result) // reject的结果 'fail'
})

事件循环

浏览器事件循环

a.JS线程读取并执行JS代码
b.执行JS代码的过程中,指定异步的操作给对应的线程处理
c.异步线程处理完毕之后,将对应的回调函数推入任务队列(多个)
d.JS线程执行完毕之后,查询任务队列,取一个任务推入JS线程运行
e: 重复b-d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
console.log(3);
Promise.resolve().then(() => {
console.log(4);
Promise.resolve().then(() => {
console.log(5);
});
console.log(6);
});
setTimeout(() => console.log(7), 0);
}, 0);

Promise.resolve().then(() => {
console.log(8);
});
console.log(9);
// 1 9 8 2 3 4 6 5 7

执行流程:

  1. 查询宏任务队列(把脚本执行当作一个宏任务来看待),取一个执行
  2. 查询微任务队列,全部执行完毕,包括当前微任务队列执行时产生的新的微任务

图片

宏任务队列

Ajax请求,绑定事件的回调函数,定时器的回调函数…

微任务队列

promsie, Object.observe, MutationObserver

总结:
  1. 先执行宏任务(script全局代码)
  2. 在执行宏任务的过程中(主进程),
    2.1 遇到宏任务(setTimeout/setInterval),把该宏任务push到宏任务队列
    2.2 遇到微任务(new Promise.then)把该微任务push到微任务队列
    2.3 执行完该宏任务后,在执行该宏任务产生的微任务队列
  3. 重复执行第二步骤,直至宏任务队列和微任务队列执行完毕,退出javascript脚本执行

浏览器的构成

图片

nodejs事件循环

图片

定时器阶段:setTimeout/setInterval
等待回调阶段:执行某些系统操作的回调,TCP错误处理
闲置阶段、准备阶段:只在内部使用
轮询阶段:I/O 回调
检查阶段:setImmediate
关闭回调阶段:关闭类的回调,socket.on(‘close’,…)

ps: 在执行每个阶段任务队列之前,都会清空process.nextTick队列和Promise队列,process.nextTick队列执行优先级更高

在事件循环poll阶段才会把I/O回调放到等待回调阶段

timers优先级最高
定时器阶段(timers):

  1. 执行优先级更高(可以理解为,用户已经等了定时器执行很久了,所以优先执行)
  2. 定时器有可能被轮询阶段正在执行的回调阻塞导致延迟(首次轮询不会有阻塞) ps: 用代码模拟一个场景

待定回调(pedding callbacks)

idle, prepare: 仅内部系统使用

轮询(poll)
检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);

setImmediate(()=> {
console.log(3);
})

console.log(4);

// 1 4 2 3 可以证明先执行timers阶段的回调
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
2. Node中的事件循环

定时器阶段:setTimeout/setInterval
等待回调阶段:执行某些系统操作的回调,TCP错误处理
闲置阶段、准备阶段:只在内部使用
轮询阶段:I/O 回调
检查阶段:setImmediate
关闭回调阶段:关闭类的回调,socket.on('close',...)

注意事项:process.nextTick > promise
每个阶段都有自己的先进先出队列,当事件循环进入到该阶段,就会执行指定的队列,直到队列被耗尽,或者达到回调的最大数量
在进入到每一个阶段之前,都会执行NextTick队列和微任务队列,前者优先级更高

3. 避免阻塞Node => 高并发(性能),DOS攻击(安全)

Node中的线程
JS引擎的线程:启动阶段,解析模块,注册事件回调,进入事件循环(执行事件回调,非阻塞的异步请求)
libuv的线程池(少量线程):操作系统不提供非阻塞版本的I/O操作(DNS),CPU密集型的任务

确保回调函数的时间复杂度为常数

可能阻塞Node的情况
糟糕的正则表达式: (a+)、(a|a)、(a.*)\1 => safe-regex
复杂JSON对象的操作 => JSONStream
复杂计算 => Child Process/Cluster

4. 异步编程方案

a. 先执行构造函数内的同步代码
b. 支持链式调用,而且then和catch返回的是全新的promise对象
c. catch只会在异步操作失败或者异常,并且前面流程未定义reject回调函数的时候触发

案例

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// p1
process.nextTick(function() { // p1
console.log(1);
});

// p2
process.nextTick(function() {
console.log(2);
setImmediate(function() {
console.log(3);
})
process.nextTick(function() {
console.log(4);
})
});

// p3
setImmediate(function() {
console.log(5);
process.nextTick(function() {
console.log(6);
});
setImmediate(function() {
console.log(7);
})
});

// p4
setTimeout(() => {
console.log(8);
new Promise((resolve, reject)=> {
console.log(9);
resolve();
}).then(e=> {
console.log(10);
});
}, 0);

// p5
setTimeout(() => {
console.log(11);
}, 0);

// p6
setImmediate(function() {
console.log(12);
process.nextTick(function() {
console.log(13);
})
process.nextTick(function(){
console.log(14);
})
setImmediate(function() {
console.log(15);
})
})

// p7
console.log(16);

// p8
new Promise((resolve, reject)=> {
console.log(17);
resolve();
}).then(e=> {
console.log(18);
})

// 执行过程分析
/*
执行结果:
poll阶段:16 17
清空nextTick队列:1 2 4
清空promise微任务队列:18
timers阶段:8 9
清空promise微任务队列:10
timers阶段:11
// timers阶段的任务已执行完,poll阶段也没有待执行的任务,开始进入checks阶段, 执行setImmediate的回调,p2的setImmediate会push到checks事件队列队尾
checks阶段: 5 // 执行一个事件对象就要重新轮询到下一个阶段
清空nextTick队列:6
checks阶段: 12
清空nextTick队列:13 14
checks阶段: 3, 7 15
结论:每个阶段每次执行完一个事件对象就重新开启事件循环

*/

nodejs事件循环6个事件

图片

图片

nodejs的构成

图片

如何证明nodejs是单线程

1
2
3
4
setTimeout(() => {
alert(1);
}, 1000);
while (true) {}

启动web服务

1
2
3
4
5
6
7
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello World!'));
app.get('/test', (req, res) => {
res.sendStatus(200);
});
app.listen(3000, () => console.log('App listening on port 3000!'));

JSON.stringify和JSON.parse执行耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 };
var len = 20;
var before, res, took, str;
for (var i = 0; i < len; i++) {
obj = { obj1: obj, obj2: obj };
}
before = process.hrtime();
str = JSON.stringify(obj);
took = process.hrtime(before);
console.log('JSON.stringify took ' + took); // 0,252061972

before = process.hrtime();
res = str.indexOf('nomatch');
took = process.hrtime(before);
console.log('Pure indexof took ' + took); // 0,16016730

before = process.hrtime();
res = JSON.parse(str);
took = process.hrtime(before);
console.log('JSON.parse took ' + took); // 0,615074412

promise

promise抛出异常或者reject错误,如果promise.then定义了异常处理函数,后面.then会走到成功回调函数

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
const flag = false;
let p1 = new Promise((resolve, reject) => {
console.log('开始异步');
throw 'error';
// setTimeout(() => {
// flag ? resolve('成功') : reject('失败');
// }, 1000);
});

p1.then(
result => {
console.log('success1: ', result);
// throw '报错啦';
return '成功回调函数的结果';
},
result => { // 处理异常函数
console.log('fail1: ', result);
return '失败回调函数的结果';
}
)
.catch(reason => {
console.log('error1:', reason);
return '异常处理结果';
})
.then(
result => {
console.log('success2: ', result);
},
result => {
console.log('fail2: ', result);
}
)
.catch(reason => {
console.log('error2:', reason);
});

nextTick循环调用,导致性能问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// nextTick每个阶段执行之前都要先清空nextTick队列
const fs = require('fs');
function addNextTickRecurs(count) {
let self = this;
if(self.id === undefined) { self.id = 0;}
if(self.id === count) return ;
process.nextTick(()=> {
console.log(`process.nextTick:${++self.id}`)
addNextTickRecurs.call(self, count);
})
}
addNextTickRecurs(1000); // 创建1000个Ticks,所以会阻塞事件循环
setTimeout(console.log.bind(console, 'setTimeout'), 10);
setImmediate(console.log.bind(console, 'setImmediate'));
// 1 2 ...1000, setTimeout, setImmediate
// 事件循环先从timers阶段开始清空任务队列

nodejs循环依赖

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
// a.js
console.log('a1--');
exports.finish = false;
const b = require('./b'); // 加在b模块,会一直把b模块执行完
console.log('a2--', b.finish); // 这里获取的是b执行完后的结果,所以是true
exports.finish = true;
console.log('a3--');

// b.js
console.log('b1--');
exports.finish = false;
const a = require('./a');
console.log('b2--', a.finish); // a还没有执行完,所以a.finish获取的是require('./b') 之前的执行结果,为false
exports.finish = true;
console.log('b3--');

// main.js
console.log('main1--');
const a = require('./a');
const b = require('./b');
console.log('main2--', a.finish, b.finish);


// 执行命令
# node a.js // a1, b1, b2--false, b3, a2--true, a3
# node main.js // maint1, a1, b1, b2--false, b3, a2--true, a3, main2--true true

???

1
2
3
// process
// Buffer
// stream

原生node启动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
const http = require("http");
const querystring = require('querystring')

const server = http.createServer((req, res)=> {
console.log('req.url--', req.url);
const params = querystring.parse(req.url.split('?')[1]); // 获取url中的参数
console.log('params--', params);

if(params.name) {
let source = Buffer.from(params.name, 'utf-8'); // 把url参数信息专门放二进制缓冲区,方便转换格式
console.log('source--', source)
let target = source.toString('hex'); // 转换为16进制数据
console.log('target--', target)
}

res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('hello world')
});

const port = 3000;
const hostname = '127.0.0.1';
server.listen(port, hostname, ()=> {
console.log('server is up...')
})

记录同步/异步错误日志

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
const { Console } = require('console');
const fs = require('fs');
const path = require('path');

const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');
const logger = new Console(output, errorOutput);
logger.log('Alan'); // 写正常日志
// logger.error('error'); // 写错误日志

// 捕获同步任务异常
try {
const fileContents = fs.readFileSync('./b.txt');
} catch (error) {
logger.error(error); // 写异常日志
}

// 捕获异步任务异常
fs.readFile('./b.txt', (err, data)=> {
try {
if(err) throw err;
console.log('data--', data);
} catch (error) {
logger.error(error);
}
})

// 读取文件路径:node ./log/index.js
// index.js
console.log(path.resolve('./')); // 执行命令所在目录:/Users/alan/project/nodejs-demo
console.log(__dirname); //执行文件所在目录: /Users/alan/project/nodejs-demo/log
console.log(__filename); // 执行文件路径: /Users/alan/project/nodejs-demo/log/index.js

// 监听服务是否退出
process.on('exit', (code)=> {
console.log(`process will close, exit code: ${code}`);
})

commonjs注入模块的变量

1
2
3
4
// module.js
(function(exports, require, module, __filename, __dirname) {
// commonjs注入以上5个变量到每个模块中
})

执行优先级对比(Promise,setImmediate,process.nextTick,setTimeout)

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
Promise.resolve().then(() => console.log('p1'));
Promise.resolve().then(() => {
console.log('p2');
Promise.resolve().then(() => console.log('p3'));
});
Promise.resolve().then(() => {
console.log('p4');
process.nextTick(() => console.log('tick1'));
});
Promise.resolve().then(() => console.log('p5'));
Promise.resolve().then(() => console.log('p6'));

// 宏任务里边的宏任务总是会放到下一轮询,微任务立即执行?
setImmediate(() => console.log('immediate1'));
setImmediate(() => {
console.log('immediate2');
process.nextTick(() => console.log('tick2'));
Promise.resolve().then(() => {
console.log('p7');
process.nextTick(() => console.log('tick3'));
});
setImmediate(() => console.log('immediate3'));
});

// 如果上面的Promis里面加个Promise试一下
process.nextTick(() => console.log('tick4'));
process.nextTick(() => {
console.log('tick5');
process.nextTick(() => console.log('tick6'));
});
process.nextTick(() => console.log('tick7'));

setTimeout(() => {
console.log('timeout1');
// 是在执行第一个定时器回调的时候添加到队列的,不是一开始就在队列
setTimeout(() => console.log('timeout2'), 0);
}, 0);

setImmediate(() => console.log('immediate4'));
setImmediate(() => console.log('immediate5'));


// 执行分析
promises队列: [p1,p2,p4, p5, p6, p3, p7]
ticks队列:[tick4,tick5, tick7, tick6, tick1, tick2, tick3]
immediate队列: [immediate1,immediate2, immediate4, immediate5, immediate3]
settimeout队列: [timeout1, timeout2]

tick4, tick5, tick7, tick6
p1, p2, p4, p5 , p6, p3
tick1,
timeout1,
immediate1, immediate2, // 当前阶段有tick或者promise都要被优先执行
tick2, p7 , tick3, // 先执行了高优先级任务,tick2添加的任务,要在下一个事件循环阶段才会被执行(immediate3)
immediate4, immediate5, // 被延后执行是因为先执行了高优先级的任务
timeout2
immediate3 // 第一轮进入immediate队列的任务是「immediate1, immediate2, immediate4, immediate5」,下一个事件循环才会被执行

总结:
1. 进入每个事件循环阶段都会优先执行微任务(process.nextTickPromise
2. 每次执行到某个阶段,会复制已完成任务队列,这就是immediate3为什么会在timeout2后面输出

对比timeout和immediate执行时机

1
2
3
4
5
6
7
8
9
10
11
// 比较 setTimeout 和 setImmediate 在不同情况下的优先级
const fs = require('fs');
const now = Date.now();

// 不在 I/O 回调内
setImmediate(() => console.log('immediate'));
// 0 => 1
setTimeout(() => console.log('timer'), 1000);
fs.readFile('./test.txt', () => console.log('readfile'));
while (Date.now() - now < 1000) {}

图片

总结:两种输出结果,主要原因是文件读取时间加while循环条件是否在1000毫秒内,第一次读取文件没有缓存,超过1000毫秒,所以先输出’timer’. 第二次执行fs读取缓存文件,小于1000毫秒,timer阶段还没有成功的回调。所以timer再后面输出。 ps:修改文件名即可看到效果~

比较setTimeout和setImmediate

1
2
3
4
5
6
7
8
9
10
11
12
13
// 比较 setTimeout 和 setImmediate 在不同情况下的优先级
const fs = require('fs');
const now = Date.now();
// 在 I/O 回调内
fs.readFile('./test.txt', () => {
console.log('readfile');
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));
});

总结:
timer阶段->i/o阶段->pedding callback阶段->poll阶段->check阶段(setImmediate)->close callback阶段->timer阶段

commonjs中this指向问题

模拟module.exports,exports和this初始化阶段指向

1
2
3
4
// var temp = {};
// module.exports = temp;
// exports = temp;
// this = temp;

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js
const review = require('./reivew.js')
console.log(review); // {count:3}

//# review.js
// var temp = {};
// module.exports = temp;
// exports = temp;
// this = temp;
exports.name = 'Alan'; // 1.temp上面加了name属性, module.exports,exports和this都指向了temp对象
module.exports = { // 2. module.exports指向了新对象{count: 3},exports和this还指向temp对象
count: 123
}
exports = { // 3. exports指向了新对象{hello: 'world'},this还指向了temp
hello: 'world'
}
console.log('this: ', this); // {name: 'Alan'}

参考文档

Nodejs进阶学习:深入了解异步I/O和事件循环
node事件循环(Event loop)