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 引擎进行处理
代码中可能会调用 nodeApi,node 会交给 libuv 库处理
libuv 通过阻塞 i/o 和多线程实现了异步 io
通过事件驱动的方式, 将结果放到事件队列中, 最终交给我们的应用
Libuv 是 Node.js 关键的一个组成部分, 它为上层的 Node.js 提供了统一的 API 调用, 使其不用考虑平台差距, 隐藏了底层实现它是一个对开发者友好的工具集, 包含定时器, 非阻塞的网络 I/O, 异步文件系统访问, 子进程等功能. 它封装了 LibevLibeio 以及 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) and setInterval(callback)预定的 callback;
I/O callbacks 阶段: 执行除了 close 事件的 callbacks 被 timers(定时器, setTimeoutsetInterval 等)设定的 callbackssetImmediate()设定的 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 方法属于微任务, 它指定的任务总是发生在所有异步任务之前 setImmediate 方法则是在当前 "任务队列" 的尾部添加事件, 也就是说, 它指定的任务总是在下一次 Event Loop 时执行
- 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 即可开始调试
另外各个编辑器也会有各自的方法可以配置自己的调试器来做调试
来源: https://juejin.im/post/5aae19b36fb9a028de447c33