Overview
Node.js 是一个基于 Chrome V8 引擎的JavaScript运行环境(runtime),Node不是一门语言,而是让js运行在后端的运行时,并且不包括javascript全集,因为在服务端中不包含DOM和BOM. Node也提供了一些新的模块例如http,fs模块等。Node.js 使用了事件驱动、非阻塞式 I/O 的模型,使其轻量又高效并且Node.js 的包管理器 npm,是全球最大的开源库生态系统。
先来张图看看node是如何工作的
- 我们写的js代码会交给v8引擎进行处理
- 代码中可能会调用Node API,Node会交给libuv库处理
- libuv通过阻塞i/o和多线程实现了异步io
- 通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用
Libuv 是 Node.js 关键的一个组成部分,它为上层的 Node.js 提供了统一的 API 调用,使其不用考虑平台差距,隐藏了底层实现。它是一个对开发者友好的工具集,包含定时器,非阻塞的网络 I/O,异步文件系统访问,子进程等功能. 它封装了 Libev、Libeio 以及 IOCP,保证了跨平台的通用性.所以实际上,Node.js 虽然说是用的 Javascript,但只是在开发时使用 Javascript 的语法来编写程序。真正的执行过程还是由 V8 将 Javascript 解释,然后由 C/C++ 来执行真正的系统调用,所以并不需要过分担心 Javascript 执行效率的问题.
上图涉及到了 Libuv 本身的一个设计理念,事件循环(Event Loop)。从这里,我们可以看到,我们其实对 Node.js 的单线程一直有个误会。事实上,它的单线程指的是自身 Javascript 运行环境的单线程,Node.js 并没有给 Javascript 执行时创建新线程的能力,最终的实际操作,还是通过 Libuv 以及它的事件循环来执行的。这也就是为什么 Javascript 一个单线程的语言,能在 Node.js 里面实现异步操作的原因,两者并不冲突。在任务调用中又可分为宏任务和微任务
macro-task(宏任务): setTimeout, setInterval, setImmediate, I/O
micro-task(微任务): process.nextTick, 原生Promise(有些实现的promise将then方法放到了宏任务中), MutationObserver
那宏任务与微任务对于执行时有什么影响吗,在浏览器与node中这两者是有区别的
console.log(1);
setTimeout(function() {
console.log(2);
Promise.resolve(1).then(function() {
console.log('promise');
})
})
setTimeout(function(){
console.log(3);
})
复制代码
以上述代码为例,在浏览器中,先默认走栈 console.log(1), 接着走第一个setTimeout,将promise微任务放到队列中,执行微任务,微任务执行完再走宏任务,所以浏览器执行时,只要一碰到微任务队列中有任务就会先去执行微任务再回来执行宏任务.这里的输出结果就会是 1 -> 2 -> promise -> 3
然而在node中,会先将宏任务队列中的任务执行完之后再去查看微任务队列并执行。这里的输出结果就会是 1 -> 2 -> 3 -> promise
REPL
在Node.js中为了使开发者方便测试JavaScript代码,提供了一个名为REPL的可交互式运行环境。开发者可以在该运行环境中输入任何JavaScript表达式,当用户按下回车键后,REPL运行环境将显示该表达式的运行结果. 在命令行容器中输入node命令并按下回车键,即可进入REPL运行环境.
在代码中我们也可以使用repl模块来帮我们创建一个repl上下文
let repl = require('repl');
let context = repl.start().context;
context.zfpx = 'zfpx';
context.age = 9;
复制代码
repl支持一些基础命令如下:
- .break 退出当前命令
- .clear 清除REPL运行环境上下文对象中保存的所有变量与函数
- .exit 退出REPL运行环境
- .save 把输入的所有表达式保存到一个文件中
- .load 把所有的表达式加载到REPL运行环境中
- .help 查看帮助命令
Console
在Node.js中,使用console对象代表控制台(在操作系统中表现为一个操作系统指定的字符界面,比如 Window中的命令提示窗口,以下列出一些基本用法:
- console.log
- console.info
- console.error
- console.warn
- console.dir
- console.time
- console.timeEnd
- console.trace
- console.assert
Node Event Loop
┌───────────────────────┐
┌─>│ timers(计时器) │
| | 执行setTimeout以及 |
| | setInterval的回调。 |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks |
│ | 处理网络、流、tcp的错误 |
| | callback |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
| | node内部使用 |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐ ┌───────────────┐
│ │ poll(轮询) │ │ incoming: │
| | 执行poll中的i/o队列 | <─────┤ connections, │
| | 检查定时器是否到时 | │ data, etc. |
│ └──────────┬────────────┘ └───────────────┘
│ ┌──────────┴────────────┐
│ │ check(检查) │
| | 存放setImmediate回调 |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks |
│ 关闭的回调例如 |
| sockect.on('close') |
└───────────────────────┘
复制代码
上面的图中描述了整个node事件循环的流程。可以看到第一张图中罗列出了多个阶段,每个阶段维护这一个观察者队列
- timers 阶段: 这个阶段执行setTimeout(callback) 和 setInterval(callback)预定的callback;
- I/O callbacks 阶段: 执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
- idle, prepare 阶段: 仅node内部使用;
- poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
- check 阶段: 执行setImmediate() 设定的callbacks;
- close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行。
事件循环除了维护这些观察者队列,还维护了一个 time 字段,在初始化时会被赋值为0,每次循环都会更新这个值。所有与时间相关的操作,都会和这个值进行比较,来决定是否执行。
在图中,与 timer 相关的过程如下:
更新当前循环的 time 字段,即当前循环下的“现在”;
检查循环中是否还有需要处理的任务(handlers/requests),如果没有就不必循环了,即是否 alive。
检查注册过的 timer,如果某一个 timer 中指定的时间落后于当前时间了,说明该 timer 已到期,于是执行其对应的回调函数;
执行一次 I/O polling(即阻塞住线程,等待 I/O 事件发生),如果在下一个 timer 到期时还没有任何 I/O 完成,则停止等待,执行下一个 timer 的回调。如果发生了 I/O 事件,则执行对应的回调;由于执行回调的时间里可能又有 timer 到期了,这里要再次检查 timer 并执行回调。
process.nextTick
process.nextTick方法属于微任务,它指定的任务总是发生在所有异步任务之前。
function Fn() {
this.arrs;
process.nextTick(() => {
this.arrs();
})
}
Fn.prototype.then = function() {
this.arrs = function() { console.log(1); }
}
let fn = new Fn();
fn.then();
复制代码
setTimeout 和 setImmediate
setImmediate在poll阶段完成时执行,即check阶段
setTimeout在poll阶段为空闲时,且设定时间到达后执行, 但其在timer阶段执行
其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的。
setTimeout(function timeout() {
console.log('timeout');
}, 0);
setImmediate(function immediate() {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
复制代码
这是因为后一个事件进入的时候,事件环可能处于不同的阶段导致结果的不确定。当我们给了事件环确定的上下文,事件的先后就能确定了。
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate');
})
})
$ node timeout_vs_immediate.js
immediate
timeout
复制代码
这是因为fs.readFile的callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout。
Debugger
V8 提供了一个强大的调试器,可以通过 TCP 协议从外部访问。Nodejs提供了一个内建调试器来帮助开发者调试应用程序。想要开启调试器我们需要在代码中加入debugger标签,当Nodejs执行到debugger标签时会自动暂停(debugger标签相当于在代码中开启一个断点)
node inspect main.js
复制代码
当然现在更流行的方式是在浏览器中进行调试,node浏览器调试可以通过chrome浏览器进行调试
node --inspect-brk main.js
复制代码
打开chrome 访问 chrome://inspect即可开始调试
另外各个编辑器也会有各自的方法可以配置自己的调试器来做调试