首页-> 博客 -> 详情

Node.js简介

作者: 苏亮 发布时间:2018-11-13

目前, 前端的主流开发框架大体分为ReactVueAngular三类。在刚接触这几个框架时, 通常你会被要求去下载并安装Node.js并通过npm源下载相关的依赖, 然后依据各个框架提供的启动命令运行你的前端服务, 从而进行下一步的前端开发。 通常在不涉及服务器端开发的工程中, 你对Node.js的应用基本上被涵盖在安装环境及依赖包的管理这两个层面。 那么, 什么是Node.js? 为什么我们一定要安装它呢? 理解Node.js对前端工程开发大有裨益。


什么是Node.js

Node.js是Javascript的运行环境, 本质上是对Chrome v8引擎进行封装。 它可以使Javascript运行在服务端, 从而使得Javascript的开发者能够编写创建服务端的逻辑, 打破了传统意义上的Javascript只能运行在客户端的限制。

我们以一个简单的Hello World示例来了解Node.js:

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

上面这个简单的例子创建了一个server, 这个server可以同时处理N个连接, 在每次连接时都将触发相应的回调, 当没有事情需要处理时, 它将进入睡眠状态。

Node.js是异步事件驱动的, 旨在构建一个可伸缩的网络应用。 与常见的使用OS线程的开发模型不同的是, Node.js是单线程的, 且其中几乎没有任何直接执行I/O的函数, 因而不用担心死锁的问题。

目前绝大多数情况下,基于Node.js的服务更适合于作为前端应用的Controller层。尽管它已经可以胜任一门服务端语言并且已存在使用Node.js编写的服务, 但其稳定性及健壮性尚未得到时间和实践的充分证明, 再加上大部分公司的后端技术已相对成熟,没有必要再花费精力将其后端服务迁移至Node.js, 因此使用Node.js开发中间层, 并以此进行前后端分离, 是大部分公司的选择。


Node.js的四大核心概念

阻塞调用与非阻塞调用

阻塞调用,即在一个的Javascript调用过程中, 实例必须等待一个非Javascript操作完成后才可以继续执行的调用, 反之则为非阻塞调用; 阻塞调用是同步执行的, 相反的, 非阻塞调用为异步执行。 在标准Node.js库中, 所有涉及到I/O的操作均提供有非阻塞调用的方式。 此外,由cpu密集(非等待非Javascript操作)引起的缺陷不称为阻塞调用。

事件循环

事件循环使得Node.js在单线程的背景下能够执行非阻塞I/O操作, 其核心原理是尽可能的将相关操作转至系统内核。

大多数现代系统内核都是多线程的, 它们可以处理在后台执行的多个操作。 当其中一个操作完成时, 内核通知Node.js将相关的回调加载到轮询机制中并执行。

以下是一个简化的事件循环的过程:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
  • timers。 在此阶段,执行setTimeout()setInterval()
  • pending callback。执行延迟到下一个循环迭代的I/O回调
  • idle, prepare。 仅内部使用
  • poll。 检索新的I/O事件, 执行相关回调
  • check。 setImmediate()在本阶段执行
  • close callbacks。 一些关闭回调

在每次执行事件循环之前, Node.js检查是否有等待的异步I/O操作或定时器, 如果没有则关闭该循环。

三项原则

本质上, Node.js使用了少量的线程去处理消费者的数量, 如果Node.js可以使用更少的线程, 则它可以将更多的系统时间和空间用于消费者的业务处理上, 因此,优雅的使用可以增加Node.js的处理速度。

  • 不阻塞事件循环。通常, 当在任何给定时间与每个消费者关联的工作量较小时, Node.js的处理速度将会快一些。这是因为事件循环通知每个新消费者连接的产生并协调起响应的建立过程, 当事件循环存在一个结点耗时过长时, 所有当前和新的消费者将无法获取其事务被处理的机会。

  • 不阻塞工作池。 在工作池队列中, 每一个实例都将在转至下一个实例之前完成其职能。 因此在处理消费者的请求时, 尽可能小化任务事件的变化, 当一个请求业务过于庞大时, 应通过拆分的方式完成该任务。

  • 谨慎添加依赖。 尽管Node.js提供了一个相对完备的生态系统, 但是每一个module的耗费资源是因开发者的不同而不同的。因此在引入新的依赖时, 应考虑是否对工作池、事件循环有影响。

如需探索更深层次的内容, 可参考Don't Block the Event Loop (or the Worker Pool)

计时器的合理使用

  • setTimeout()——“我说的时候”执行体, 类似于浏览器端的window.setTimeout(), 主要用于在特定时间后执行代码。

  • setImmediate()——“立即”执行体, 相关代码在当次事件循环周期的末尾执行。

  • setInterval()——“循环”执行体, 如字面意思解释的, 相关代码每隔一个给定的时间段执行一次。

  • 计时器的清除。 setTimeout()setImmediate()以及setInterval()均返回一个timer对象, 因此可以通过Node.js提供的清空方法清除该对象, 示例如下:

const timeoutObj = setTimeout(() => {
  console.log('timeout beyond time');
}, 1500);

const immediateObj = setImmediate(() => {
  console.log('immediately executing immediate');
});

const intervalObj = setInterval(() => {
  console.log('interviewing the interval');
}, 500);

clearTimeout(timeoutObj);
clearImmediate(immediateObj);
clearInterval(intervalObj);

参考资料

本文节选,译自Node官方文档, 如有纰漏请联系我们

  1. Node.js教程

  2. 关于Node.js