Skip to content

nodeJS

Node.js 特点

  • Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它具有以下特点:

  • 非阻塞式 I/O:Node.js 使用事件驱动的、非阻塞式 I/O 模型,可以处理大量并发连接而不需要创建多线程,这使得它非常适合处理 I/O 密集型的应用程序。

  • 单线程:尽管 Node.js 是单线程的,但它通过事件循环机制和异步编程模型来实现并发处理,使得可以高效地处理大量请求。

  • 事件驱动:Node.js 基于事件驱动的架构,通过回调函数来处理异步操作,如文件 I/O、网络请求等,使得开发者能够编写具有高响应性的应用程序。

  • 跨平台:Node.js 可以运行在多种操作系统上,如 Windows、Linux、macOS 等,使得开发者可以轻松地在不同的平台上部署应用程序。

  • 轻量高效:Node.js 的设计目标之一是轻量高效,它的模块化机制使得开发者可以根据需求选择合适的模块,从而减小应用程序的体积并提升运行效率。

  • 丰富的生态系统:Node.js 拥有庞大而活跃的生态系统,提供了丰富的第三方模块和工具,使得开发者可以快速构建复杂的应用程序。

  • 容易学习:由于 Node.js 使用 JavaScript 作为开发语言,因此对于前端开发者而言,学习 Node.js 相对较容易,可以实现前后端统一开发。

  • 支持实时应用程序:Node.js 通过 WebSocket 等技术支持实时通信,使得开发者可以轻松构建实时应用程序,如聊天应用、在线游戏等

nodejs 事件循环

  • 0、在执行事件循环前,会先执行同步任务、在执行process.nextTick 、微任务
  • 1、timers: 这个阶段执行timer(setTimeout、setInterval)的回调,该阶段新创建的setTimeout、setInterval 会继续放在下一个轮回循环阶段执行
  • 2、pending callbacks: 执行一些系统操作的回调(比如网络通信的错误回调);
  • 3、idle, prepare: 仅node内部使用;
  • 4、 poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里(比如:文件读取操作,http请求 回调 ) 底层代码在poll阶段执行的时候,会传入一个timeout超时时间,timeout超时时间到了,则退出poll阶段,执行下一个阶段
  • 5、 check: setImmediate()回调函数在这里执行。
  • 6、 close callbacks:比如socket.on(‘close’, callback)的callback会在这个阶段执行。
js
    ┌───────────────────────────┐
 ┌─>│           timers          │ 本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
 │  └─────────────┬─────────────┘
 │  ┌─────────────┴─────────────┐
 │  │     pending callbacks     │ (系统内部)执行一些系统操作的回调
 │  └─────────────┬─────────────┘
 │  ┌─────────────┴─────────────┐
 │  │       idle, prepare       │ 仅node内部使用
 │  └─────────────┬─────────────┘      ┌───────────────┐
 │  ┌─────────────┴─────────────┐      │   incoming:   │
 │  │           poll            │<─────┤  connections, │ I/O 回调下node将阻塞,文件读取操作,http请求 回调
 │  └─────────────┬─────────────┘      │   data, etc.  │
 │  ┌─────────────┴─────────────┐      └───────────────┘
 │  │           check           │ setImmediate()回调函数在这里执行。
 │  └─────────────┬─────────────┘
 │  ┌─────────────┴─────────────┐
 └──┤      close callbacks      │ 比如socket.on(‘close’, callback)的callback会在这个阶段执行。
    └───────────────────────────┘

nodejs 事件循环和浏览器的区别?

  • 浏览器只有一个宏任务队列
  • node 会开启多个宏任务队列,setTimeout=>timers、setImmediate=>check

nextTick

  • process.nextTick()不在 event loop 的任何阶段执行,而是等执行栈中的同步代码执行完成后立马执行,比微任务还要早,如果在此阶段执行同步的任务,会阻塞后续的执行
js
function read() {
  process.nextTick(function () {
    console.log(1);
    // 阻塞事件循环
    while (true) {}
  });
  Promise.resolve().then(() => {
    console.log("Promise");
  });
  console.log(0);
}
read(); //0 1

setTimeout(0)对比 setImmediate()

  • setImmediate() 设计为一旦在当前 轮询 check 阶段就执行回调
  • setTimeout() 等待到达时间后立马执行回调
js
// 如果运行以下不在 I/O 周期(即主模块)内的脚本,setTimeout 0和setImmediate,它们的执行顺序具有不确定性
// 受计算器性能和其他程序的影响,到事件循环到timer阶段时,setTimeout 0不一定到达了时间,如果时间未到会达到下一个阶段

// setTimeout 0不一定是0秒
setTimeout(() => {
  console.log("timeout");
}, 0);

setImmediate(() => {
  console.log("immediate");
});
js
// 但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用:
const fs = require("fs");

// I/O的回调在poll被执行,poll执行结束后,
// 如果会setImmediate会进入check执行回调,所以在I/O操作中setImmediate 总是被优先调用
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log("timeout");
  }, 0);
  setImmediate(() => {
    console.log("immediate");
  });
});

exec, execFile, spawn, execSync, execFileSync, spawnSync

  • 都可以执行操作系统原生的系统命令,也可以执行 shell 脚本

  • error:如果命令执行失败,则该参数为一个 Error 对象,否则为 null。

  • stdout:命令的标准输出。是一个包含命令执行结果的字符串。

  • stderr:命令的标准错误输出。也是一个包含命令执行结果的字符串。

js
const path = require("path");
const {
  exec,
  execFile,
  spawn,
  fork,
  execSync,
  execFileSync,
  spawnSync,
} = require("child_process");

function execHandler() {
  // const execChild = exec( 'ls -al | grep node_modules', function ( err, stdout, stderr ) {
  //   console.log( 'callback start-------------' );
  //   console.log( stderr );
  //   console.log( stdout );
  //   console.log( err );
  //   console.log( 'callback end-------------' );
  // } );

  const execChild = exec(
    path.resolve(__dirname, "test.shell"),
    function (err, stdout, stderr) {
      console.log("callback start-------------");
      console.log(stderr);
      console.log(stdout);
      console.log(err);
      console.log("callback end-------------");
    }
  );

  execChild.stdout.on("data", (chunk) => {
    console.log("stdout data", chunk);
  });

  execChild.stdout.on("close", () => {
    console.log("stdout close");
  });

  execChild.on("error", (err) => {
    console.log("error!", err);
  });

  execChild.stderr.on("data", (chunk) => {
    console.log("stderr data", chunk);
  });

  execChild.stderr.on("close", () => {
    console.log("stderr close");
  });
}

function execFileHandler() {
  // const execChild = execFile( 'ls', [ '-al' ], function ( err, stdout, stderr ) {
  //   console.log( 'callback start-------------' );
  //   console.log( stderr );
  //   console.log( stdout );
  //   console.log( err );
  //   console.log( 'callback end-------------' );
  // } );

  const execChild = execFile(
    path.resolve(__dirname, "test.shell"),
    function (err, stdout, stderr) {
      console.log("callback start-------------");
      console.log(stderr);
      console.log(stdout);
      console.log(err);
      console.log("callback end-------------");
    }
  );

  execChild.stdout.on("data", (chunk) => {
    console.log("stdout data", chunk);
  });

  execChild.stdout.on("close", () => {
    console.log("stdout close");
  });

  execChild.on("error", (err) => {
    console.log("error!", err);
  });

  execChild.stderr.on("data", (chunk) => {
    console.log("stderr data", chunk);
  });

  execChild.stderr.on("close", () => {
    console.log("stderr close");
  });
}

function spawnHandler() {
  const spawnChild = spawn("cnpm", ["install"], {
    cwd: path.resolve(__dirname),
    stdio: "inherit", // inherit 传出父进程
  });
  // inherit模式下无法监听事件
  spawnChild.stdout.on("data", (chunk) => {
    console.log("stdout data", chunk.toString());
  });

  spawnChild.stdout.on("close", () => {
    console.log("stdout close");
  });

  spawnChild.on("error", (err) => {
    console.log("error!", err);
  });

  spawnChild.stderr.on("data", (chunk) => {
    console.log("stderr data", chunk.toString());
  });

  spawnChild.stderr.on("close", () => {
    console.log("stderr close");
  });
}
// 同步会阻塞
const ret = execSync("ls -al");
console.log(ret.toString());

const ret2 = execFileSync("ls", ["-al"]);
console.log(ret2.toString());

const ret3 = spawnSync("ls", ["-al"]);
console.log(ret3.stdout.toString());

exec、spawn 区别?

  • exec:会一次性返回输出执行结果内容

    • 适合用消耗比较小的
  • spawn:基于事件流程的方式,一点点的返回输出执行结果内容

    • 适合用在处理大量数据返回的场景中,图片处理,读二进制数据等等。

fork 主进程/子进程间通信

  • fork 负责在两个 Node. js 程序( JavaScript)之间进行交互。
js
// parent
let { fork } = require("child_process");
let path = require("path");
const forkChild = fork(path.resolve(__dirname, "child.js"));

// 给子进程发生消息
forkChild.send("hello child process!");

// 监听子进程的消息
forkChild.on("message", (msg) => {
  console.log(msg);
});
js
// child
console.log("child process");
console.log("child pid:", process.pid);

// 监听父进程的消息
process.on("message", (msg) => {
  console.log(msg);
});

// 给父进程发生消息
process.send("hello main process");

Cluster 集群

  • Javascript 是单线程的,如果用 nodeJS 做 web service,无法享受到 CPU 多核的好处

  • Cluster 模块可以让单实例的 NodeJs 应用程序,运行在多个子进程下,每个进程监听同一个 端口,通过主进程分配给子进程任务,到达负载 web service

cluster

js
// cluster.js
const cluster = require("node:cluster");
const http = require("node:http");
const numCPUs = require("node:os").availableParallelism();
const process = require("node:process");
if (cluster.isPrimary) {
  console.log(`主进程 ${process.pid} 上线`);
  for (let i = 0; i < numCPUs; i++) {
    // 相当于node main.js,重新执行自己
    // 和process_child相比,不用重新创建child.js,
    let worker = cluster.fork();
    worker.send("来自主进程的消息");
  }

  Object.keys(cluster.workers).forEach((worker) => {
    cluster.workers[worker].on("message", (msg) => {
      console.log(`接收子进程的消息:${msg}`);
    });
  });

  cluster.on("online", function (worker) {
    console.log(`子进程 ${worker.process.pid} 上线`);
  });

  cluster.on("exit", (worker, code, signal) => {
    console.log(`子进程 ${worker.process.pid} 死亡`);
    // 开启新的子进程(可以通过kill -9 pid杀掉进程观察)
    cluster.fork();
  });
} else {
  http
    .createServer((req, res) => {
      res.writeHead(200);
      res.end("hello world\n");
    })
    .listen(9999);

  process.on("message", function (msg) {
    console.log(`接收主进程的消息:${msg}`);
  });
  process.send("来自子进程的消息");
}
npm
huleideMacBook-Pro:cluster hl$ node index.js
主进程 29549 上线
子进程 29550 上线
子进程 29555 上线
子进程 29557 上线
子进程 29556 上线
子进程 29553 上线
子进程 29551 上线
子进程 29552 上线
子进程 29558 上线
子进程 29554 上线
子进程 29559 上线
接收主进程的消息:来自主进程的消息
接收主进程的消息:来自主进程的消息
接收子进程的消息:来自子进程的消息
接收子进程的消息:来自子进程的消息
接收子进程的消息:来自子进程的消息
接收主进程的消息:来自主进程的消息
接收子进程的消息:来自子进程的消息
接收子进程的消息:来自子进程的消息
接收主进程的消息:来自主进程的消息
接收主进程的消息:来自主进程的消息
接收子进程的消息:来自子进程的消息
接收主进程的消息:来自主进程的消息
接收子进程的消息:来自子进程的消息
接收主进程的消息:来自主进程的消息
接收子进程的消息:来自子进程的消息
接收主进程的消息:来自主进程的消息
接收主进程的消息:来自主进程的消息
接收主进程的消息:来自主进程的消息
接收子进程的消息:来自子进程的消息
接收子进程的消息:来自子进程的消息

huleideMacBook-Pro:~ hl$ kill -9 29559
子进程 29559 死亡
子进程 29619 上线

Nodejs如何处理优雅退出

信号

  • SIGTERM 用于终止程序.它可以友好的要求一个程序终止。程序既可以响应这个信号,清理资源并退出,又可以忽略这个信号。

  • SIGKILL 用于立刻终止程序.与 SIGTERM 信号不同,进程不能响应或忽略这个信

  • SIGINT 在用户键入INTR 字符(通常是Ctrl-C)时发出,用于通知进程终止进程。

  • SIGQUIT 和 SIGINT 类似,但由QUIT 字符(通常是Ctrl-V来控制.进程在因收到SIGQUIT 退出时会产生 core 文件,在这个意义上类似于一个程序错误信号。

  • Node.js Http 模块带有一个 close 方法,可以在处理完所有请求后停止响应新的连接,并触发回调函数。这个方法来自 NET 模块,因此对于所有类型的tcp 连接你都可以很方便的使用这个方法。

  • SIGTERM 和 SIGINT 在非Windows 平台上有默认的响应方式,会首先以128+信号的退出代码来退出」之后重置命令行.如果某个信号在程序中被监听,则默认的处理行为会被覆盖(Node 不会退出)

js
// 通过process监听操作系统发出的终止进程信号
// 利用Http 模块带有一个 close 方法,在处理完所有请求后停止响应新的连接,并且退出nodejs进程
const http = require('http');

const server = http.createServer(function (req, res) {
  setTimeout(function () {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World\in');
  }, 4000)
}).listen(9090, function () {
  console.log('listening <http://localhost:9090/>')
  console.log('pid is ' + process.pid)
})

// 监听操作系统发出的终止进程信号
process.on("SIGTERM", function () {
  console.log("Node process stopping...")
  // 关闭http连接
  server.close(function () {
    console.log("HTTP server stopped")
    // 退出nodejs进程
    process.exit(0)
  })
})

Koa洋葱模型

js
let middleware =[]

middleware.push(function(next){
  console.log('A')
  next()
  console.log('A1')
})

middleware.push(function(next){
  console.log('B')
  next()
  console.log('B1')
})



middleware.push(function(next){
  console.log('C')
  next()
  console.log('C1')
})


function compose(middleware){
  function dispatch(index){
    middleware[index] && middleware[index](()=> dispatch(index+1))
  }
  return ()=>{
    dispatch(0)
  }

}
compose(middleware)() // A B C C1 B1 A1

Koa2和Koa1的区别,和express的区别?

  • 1、异步流程控制

    • Express 采用 callback 来处理异步,Koa v1 采用 generator,
    • Koa v2 采用 async/await。
  • 2、错误处理

    • Express 使用 callback 捕获异常,对于深层次的异常捕获不了,
    • Koa 使用 try catch,能更好地解决异常捕获。

进程

  • 进程是计算机中运行的程序的实例它是操作系统进行资源分配和管理的基本单位。每个进程都有自己的地址空间内存文件和系统资源,它们之间相互独立运行。进程可以看作是程序的一次执行过程,包括程序的代码、数据和执行状态。操作系统通过调度进程的执行来实现多任务和并发处理。

  • 在多道程序设计环境中,多个进程可以同时存在并且并发执行。每个进程都有自己的执行流,独立于其他进程。进程之间可以通过进程间通信(IPC)来进行数据交换和同步操作。

  • 总体来说,进程是计算机系统中用于执行程序的一种抽象概念,它使得多个任务能够并行运行,提高了系统的效率和响应速度。

线程

  • 线程是在进程内部独立运行的基本单元。一个进程可以包含多个线程,它们共享相同的资源,如内存空间和文件句柄,但每个线程都有自己的执行流。线程是操作系统调度的基本单位,相对于进程而言,线程更轻量级。

  • 在单线程模型中,程序只有一个执行流,而在多线程模型中,程序可以同时执行多个线程,每个线程执行不同的任务。线程之间可以共享数据,也可以通过同步机制来协调彼此的操作。由于线程共享同一地址空间,线程间通信相对于进程间通信更为简便。

  • 线程的使用可以提高程序的并发性和响应速度,尤其在多核处理器系统中能够更有效地利用计算资源。然而,多线程编程也带来了一些挑战,如竞态条件和死锁等问题,需要谨慎处理。

  • 总的来说,线程是进程内的执行单元,可以并发执行,共享进程的资源,是实现多任务和并发编程的重要手段。

进程和线程的区别

基本定义:

  • 进程是程序的一次执行过程,拥有独立的内存空间、文件和系统资源。
  • 线程是在进程内部独立运行的执行单元,共享进程的资源,包括内存空间。

资源开销:

  • 进程之间的切换需要保存和恢复整个进程的上下文,因此进程的资源开销较大。
  • 线程切换只需要保存和恢复线程私有的上下文,开销相对较小。

通信与共享:

  • 进程通常通过进程间通信(IPC)来进行数据交换,如管道、消息队列等。
  • 线程之间共享进程的地址空间,因此可以通过共享内存等方式方便地进行通信。

独立性:

  • 进程是独立的执行实体,一个进程的崩溃通常不会影响其他进程。
  • 线程共享同一地址空间,一个线程的错误可能导致整个进程的崩溃。

创建和销毁:

  • 进程的创建和销毁相对较慢,涉及到较多的系统资源分配和释放。
  • 线程的创建和销毁较快,因为它们共享进程的资源。

并发性:

  • 多个进程可以并发执行,各自独立运行。

  • 多线程在同一进程内并发执行,共享相同的资源,需要考虑同步和互斥。

  • 总的来说,线程是轻量级的执行单元,共享进程资源,适合在同一任务内进行并发处理,而进程是独立的执行环境,适合在不同任务之间进行并发。选择使用进程还是线程取决于具体的应用需求和设计考虑。