跳到主要内容前端开发核心计算机基础知识梳理 | 极客日志JavaScriptNode.js大前端算法
前端开发核心计算机基础知识梳理
前端开发核心计算机基础知识梳理涵盖进程线程、IPC、内存管理及网络协议等关键内容。重点解析 Node.js 多进程模型、V8 垃圾回收机制及 TCP/UDP 应用场景,助力开发者深入理解底层原理并优化实际项目性能。
DevOpsTeam15 浏览 进程和线程的区别
简单理解:进程是'独立的容器',线程是'容器里干活的人'。多人共享容器资源,效率更高但也更容易互相影响。
进程:独立可运行的程序,比如微信、VSCode。它是操作系统资源分配的最小单位(包括内存、CPU 时间片、文件句柄等),每个进程都有自己独立的内存空间,进程之间互不干扰。
线程:是进程的执行单位,一个进程可以包含多个线程。例如在微信进程中,有接收消息线程、渲染界面线程。线程是调度执行的最小单位,同一进程内的线程共享进程的内存和资源。
类比:进程像一家'独立的公司',有自己的办公场地(内存)、资金(系统资源);线程像公司里的'员工',共享公司的场地和资金,各自做不同的工作,协作完成公司整体任务。
| 维度 | 进程 | 线程 |
|---|
| 资源分配 | 系统资源分配的最小单位 | 资源调度 / 执行的最小单位 |
| 内存空间 | 每个进程有独立的内存空间 | 共享所属进程的内存空间 |
| 通信方式 | 复杂(需 IPC:管道、套接字、共享内存等) | 简单(直接读写进程内共享变量) |
| 创建 / 销毁开销 | 大(需分配独立内存、资源) | 小(仅需栈空间,共享进程资源) |
| 独立性 | 高(进程崩溃不影响其他进程) | 低(线程崩溃可能导致整个进程崩溃) |
| 切换开销 | 大(需切换地址空间、资源上下文) | 小(仅切换执行上下文) |
| 控制权 | 由操作系统内核管理 | 可由进程自己管理或内核管理 |
1. 进程的使用场景
- 独立运行的程序:比如你写的一个 UniApp 打包后的 APP、一个 Python 脚本单独运行,都是一个进程。
- 需隔离资源的场景:比如服务器上的多个服务(数据库、后端接口、前端静态服务),用不同进程运行,避免一个服务崩溃影响其他服务。
2. 线程的使用场景
- 同一任务的并行处理:比如称重代码中,串口数据接收线程、定时读取重量线程,都属于同一个 APP 进程内的线程,共享串口资源和重量数据。
- 耗时操作不阻塞主线程:比如在 UniApp 中,网络请求、串口通信都要放在子线程,避免阻塞 UI 渲染线程(主线程),否则界面会卡顿。
进程间的通信 (IPC)
Node.js 中的进程通讯主要分为两类:父子进程通信和无亲缘关系进程通信。
进程间通信(Inter-Process Communication,IPC)是指在操作系统中,不同进程之间交换信息和数据的过程。常见的进程通信方式包括:
- 内置 IPC 通道:父子进程专用
- 管道(Pipe):无亲缘关系进程通信
- 网络套接字(Socket):无亲缘关系进程通信
- 文件 / 数据库(Redis 发布订阅):无亲缘关系进程通信
| 方式 | 适用场景 | 优点 | 缺点 |
|---|
| 内置 IPC 通道 | 父子进程(Node.js 间) | 高效、原生、使用简单 | 仅父子进程、不跨机器 |
| 管道 | 父子进程 / 本地进程 | 轻量、系统原生 | 仅本地、单向 / 半双工 |
| TCP Socket | 任意进程(跨机器 / 跨语言) | 通用、跨平台、实时性好 | 有网络开销、需处理连接 |
| Redis 发布订阅 | 跨进程 / 跨机器 | 解耦、易扩展 | 依赖 Redis 服务、有延迟 |
1. 父子进程通信
Node.js 的 child_process 模块(尤其是 fork() 方法)内置了高效的 IPC 通道,这是实现父子进程通信最便捷的方式。
const { fork } = require("child_process");
var fork1 = fork("./common/ChildIpc");
fork1.on("message", message => {
console.log(`父进程收到子进程消息:`, message);
});
fork1.send({ data: 123 });
setTimeout(() => {
console.log("123");
}, 5000);
process.on("message", msg => {
console.log("收到主进程的消息", msg);
});
另外 cluster 模块基于 child_process.fork() 实现,专门用于多核 CPU 利用,主进程和工作进程之间同样通过 send()/message 通信。
2. 无亲缘关系进程通信
(1) 管道(Pipe)
管道是操作系统提供的基础 IPC 机制,分为匿名管道(仅父子进程)和命名管道(任意进程)。Node.js 中 child_process 的 spawn/exec 方法默认会创建管道,用于标准输入 / 输出(stdin/stdout/stderr)通信。
const { spawn } = require('child_process');
var childProcessWithoutNullStreams = spawn('node', ["./common/ChildIpc.js"]);
childProcessWithoutNullStreams.stdin.write("父进程通过管道发送的数据");
childProcessWithoutNullStreams.stdin.end();
childProcessWithoutNullStreams.stdout.on('data', (data) => {
console.log('父进程收到子进程回复:', data.toString());
});
setTimeout(() => { console.log("123") }, 5000);
process.stdin.on("data", (data) => {
console.log("收到主进程管道的消息", Buffer.from(data).toString());
process.stdout.write("反馈");
});
命名管道:
命名管道本质是文件系统中的一个特殊文件(Linux/macOS 是 FIFO 文件,Windows 是 \\.\pipe\ 格式的管道名),不同进程可以通过读写这个'文件'来交换数据,它是双向通信的,且通信效率远高于网络套接字。
const fs = require('fs');
const net = require('net');
const os = require('os');
const PIPE_PATH = os.platform() === 'win32' ? '\\\\.\\pipe\\my-node-pipe' : '/tmp/my-node-pipe';
function createFifoIfNeeded() {
if (os.platform() !== 'win32') {
try {
fs.mkfifoSync(PIPE_PATH, 0o666);
console.log(`创建 FIFO 文件:${PIPE_PATH}`);
} catch (err) {
if (err.code === 'EEXIST') {
console.log(`FIFO 文件已存在:${PIPE_PATH}`);
} else {
throw err;
}
}
}
}
function startPipeServer() {
createFifoIfNeeded();
if (os.platform() === 'win32') {
const server = net.createServer((connection) => {
console.log('客户端已连接(Windows 管道)');
connection.on('data', (data) => {
console.log(`收到客户端消息:${data.toString().trim()}`);
connection.write(`服务端已收到:${data.toString().trim()}\n`);
});
connection.on('end', () => { console.log('客户端断开连接'); });
});
server.listen(PIPE_PATH, () => {
console.log(`Windows 命名管道服务端启动:${PIPE_PATH}`);
});
} else {
const readStream = fs.createReadStream(PIPE_PATH);
readStream.setEncoding('utf8');
console.log(`Linux/macOS FIFO 服务端启动:${PIPE_PATH}`);
readStream.on('data', (data) => {
const msg = data.toString().trim();
console.log(`收到客户端消息:${msg}`);
const writeStream = fs.createWriteStream(PIPE_PATH, { flags: 'a' });
writeStream.write(`服务端已收到:${msg}\n`);
writeStream.end();
});
readStream.on('close', () => { console.log('FIFO 管道关闭'); });
}
}
startPipeServer();
process.on('SIGINT', () => {
if (os.platform() !== 'win32' && fs.existsSync(PIPE_PATH)) {
fs.unlinkSync(PIPE_PATH);
console.log(`已删除 FIFO 文件:${PIPE_PATH}`);
}
process.exit(0);
});
const fs = require('fs');
const net = require('net');
const os = require('os');
const PIPE_PATH = os.platform() === 'win32' ? '\\\\.\\pipe\\my-node-pipe' : '/tmp/my-node-pipe';
function sendMessage(msg) {
if (os.platform() === 'win32') {
const client = net.connect(PIPE_PATH, () => {
console.log('已连接到 Windows 命名管道服务端');
client.write(msg + '\n');
});
client.on('data', (data) => {
console.log(`服务端回复:${data.toString().trim()}`);
client.end();
});
client.on('error', (err) => { console.error('连接失败:', err.message); });
} else {
if (!fs.existsSync(PIPE_PATH)) {
console.error(`FIFO 文件不存在:${PIPE_PATH}`);
return;
}
const writeStream = fs.createWriteStream(PIPE_PATH, { flags: 'a' });
writeStream.write(msg + '\n');
writeStream.end(() => {
console.log('客户端消息已发送');
const readStream = fs.createReadStream(PIPE_PATH);
readStream.setEncoding('utf8');
readStream.on('data', (data) => {
console.log(`服务端回复:${data.toString().trim()}`);
readStream.destroy();
});
});
}
}
const message = process.argv[2] || '默认消息:Hello Node.js 命名管道';
sendMessage(message);
| 系统 | 管道路径格式 | 实现方式 |
|---|
| Windows | \\.\pipe<管道名> | 基于 net 模块 |
| Linux/macOS | /tmp/<管道名> | 基于 fs 模块读写 FIFO |
(2) Socket
const net = require('net');
const server = net.createServer((socket) => {
console.log('有进程连接成功');
socket.on('data', (data) => {
console.log('服务端收到:', data.toString());
socket.write('服务端已收到消息:' + data.toString());
});
});
server.listen(3000, '127.0.0.1', () => {
console.log('TCP 服务端启动,监听 3000 端口');
});
const client = net.connect({ port: 3000, host: '127.0.0.1' }, () => {
console.log('客户端连接成功');
client.write('进程 B 发送的消息');
});
client.on('data', (data) => {
console.log('客户端收到:', data.toString());
client.end();
});
(3) 共享存储 (Redis 发布订阅)
const { createClient } = require('redis');
const publisher = createClient();
publisher.connect();
setInterval(async () => {
const msg = `当前时间:${new Date().toLocaleString()}`;
await publisher.publish('process-channel', msg);
console.log('发布者发送:', msg);
}, 1000);
const subscriber = createClient();
subscriber.connect();
subscriber.subscribe('process-channel', (msg) => {
console.log('订阅者收到:', msg);
});
线程间的通信
和进程间通信不同,Node.js 的工作线程虽然有独立的 V8 实例和内存,但支持两种通信模式:消息传递(默认)和共享内存。
1. 基础方式:主线程 ↔ 工作线程(消息传递)
const { Worker } = require("worker_threads");
var worker = new Worker("./common/ChildThread.js", { workerData: { num: 1e9 } });
worker.on('message', (result) => {
console.log('工作线程计算结果:', result);
worker.terminate();
});
worker.on('error', (err) => { console.error('工作线程出错:', err); });
worker.on('exit', (code) => { if (code !== 0) { console.error(`工作线程退出,码值:${code}`); } });
worker.postMessage({ data: { num: 1e9 } });
console.log('主线程不阻塞,继续执行');
const { parentPort } = require("worker_threads");
parentPort.on('message', message => {
console.log("收到主线程的消息", message);
parentPort.postMessage("xxxxxxxxxxxxxx");
});
2. 进阶方式:工作线程 ↔ 工作线程(直接通信)
const { Worker, MessageChannel } = require("worker_threads");
var worker = new Worker("./common/ChildThread.js");
var worker1 = new Worker("./common/ChildThread1.js");
let { port1, port2 } = new MessageChannel();
worker1.on('message', (result) => {
console.log('工作线程计算结果:', result);
worker.terminate();
worker1.terminate();
});
const { parentPort } = require("worker_threads");
let worker2;
parentPort.on("message", message => {
worker2 = message.port;
worker2.on('message', (data) => {
console.log(`worker2 收到 worker1 的消息:${data}`);
parentPort.postMessage("dfdf");
});
});
let worker1;
parentPort.on('message', message => {
worker1 = message.port;
worker1.on('data', (data) => {
console.log(`worker2 收到 worker1 的消息:${data}`);
worker1.postMessage('hello worker1,我是 worker2!');
});
worker1.postMessage('hello worker2,我是 worker1');
});
3. 高性能方式:共享内存(SharedArrayBuffer)
通过 SharedArrayBuffer 实现线程间内存共享,数据无需复制,适合大数据量传输,但需要注意并发安全。
const { Worker } = require('worker_threads');
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Uint32Array(sharedBuffer);
sharedArray[0] = 0;
const worker1 = new Worker('./worker-shared.js', { workerData: { sharedBuffer, id: 1 } });
const worker2 = new Worker('./worker-shared.js', { workerData: { sharedBuffer, id: 2 } });
setTimeout(() => {
console.log(`共享内存最终值:${sharedArray[0]}`);
worker1.terminate();
worker2.terminate();
}, 1000);
const { workerData } = require('worker_threads');
const { sharedBuffer, id } = workerData;
const sharedArray = new Uint32Array(sharedBuffer);
for (let i = 0; i < 1000; i++) {
Atomics.add(sharedArray, 0, 1);
}
console.log(`线程 ${id} 累加完成`);
单核 CPU 如何实现并发
单核 CPU 主要是通过时间片轮转和上下文切换来实现并发。
时间片轮转:CPU 将时间划分为很小的时间片(通常是几十毫秒),每个进程、线程分配到一个时间片,CPU 轮流执行。当一个时间片用完,CPU 就会切换到下一个进程、线程。
上下文切换:在切换时,CPU 需要保存当前进程的状态(程序计数器、寄存器、内存映射信息),然后加载下一个要执行的进程、线程的上下文。
在任意时刻,CPU 只能执行一个任务,由于切换速度非常快,给用户的感觉就像是在同时运行多个程序,所以这种机制被称为'伪并发'。若线程过多也不好,频繁的上下文切换会带来一定的性能开销,所以过多的线程反而会带来性能下降的问题。
CPU 调度算法有哪些?
- 先来先服务(FCFS):最简单的调度算法,按到达顺序排队。缺点是长作业可能阻塞后续短作业。
- 短作业优先(SJF):优先执行预计运行时间最短的进程。可能导致'饥饿'现象。
- 优先级调度:根据优先级执行,通常配合老化技术解决饥饿问题。
- 轮转调度(RR):每个进程分配固定时间片,适用于时间共享系统。
- 多级队列/反馈队列调度:将进程分类或动态调整优先级,更灵活适应不同负载。
Linux 如何查找你的进程占用的那个端口
netstat -tunlp
netstat -tunlp | grep 8080
fuser 8080/tcp
fuser 8080/udp
kill 805
编译型语言和解释型语言的区别
- 编译型语言:就像先请翻译把整本外文书翻译成中文书,之后直接看翻译好的就行。程序运行前,通过编译器将源代码一次性翻译成机器能直接执行的二进制指令,后续运行时不再需要源代码。
- 解释型语言:就像请翻译逐句给你解释外文书,你看一句,翻译解释一句。程序运行时,由解释器逐行读取源代码、逐行解释执行,不会生成独立的二进制可执行文件。
| 维度 | 编译型语言 | 解释型语言 |
|---|
| 执行流程 | 先编译 → 后运行 | 边解释 → 边运行 |
| 运行效率 | 快(直接执行机器码) | 慢(每次都要解释) |
| 开发调试效率 | 编译报错后需修改重新编译 | 改代码后可立即运行 |
| 跨平台性 | 强绑定平台 | 只要有对应解释器即可 |
| 代表语言 | C、C++、Go、Rust、Java | Python、JavaScript、PHP |
JIT(Just-In-Time)编译是为了结合两者优点而诞生的,它在程序运行时将部分代码编译成机器码,现代 JavaScript 引擎(如 V8)和 JVM 都广泛使用 JIT 来提高性能。
栈与堆
在 JavaScript 中,所有变量和数据都会被存储在内存中,栈和堆是两种不同的内存分配区域:
- 栈(Stack):类似'叠盘子',遵循「先进后出(LIFO)」原则,存储占用空间固定、大小已知的数据,访问速度极快。
- 堆(Heap):类似'开放式仓库',存储占用空间不固定、大小动态变化的数据,访问速度相对慢,但灵活性高。
1. 栈(Stack)—— 存储基础类型 + 引用类型的指针
JavaScript 的基本数据类型会直接存在栈中:Number、String、Boolean、Undefined、Null、Symbol、BigInt。特点:占用空间小且固定,赋值时会复制整个值(值传递)。
此外,函数调用的执行上下文也会存在栈中,函数执行完后会被立即弹出栈,释放内存。
let a = 10;
let b = a;
b = 20;
console.log(a);
console.log(b);
2. 堆(Heap)—— 存储引用类型
JavaScript 的引用类型会存储在堆中,栈中仅保存指向堆内存的「指针(地址)」:Object(包括数组、对象、函数、正则等)。特点:占用空间大且动态变化,赋值时仅复制指针(引用传递)。
let obj1 = { name: "张三" };
let obj2 = obj1;
obj2.name = "李四";
console.log(obj1.name);
console.log(obj2.name);
obj2 = { name: "王五" };
console.log(obj1.name);
console.log(obj2.name);
- 栈内存:函数执行上下文出栈时,对应的变量会被立即释放,无需垃圾回收。
- 堆内存:当一个对象不再被任何变量引用时,垃圾回收器会在合适时机清理这块内存,避免内存泄漏。
JS 垃圾回收的过程与算法
JS 引擎就像一个管家,内存是家里的储物空间。垃圾回收就是管家定期清理那些再也用不到的'杂物',释放空间避免堆积。
核心原则:找出不再使用的变量 / 对象,释放其占用的内存。
1. 引用计数法(早期算法,有缺陷)
原理:跟踪每个值被引用的次数。当引用数变为 0 时,说明该值无法被访问,会被回收。
缺陷:无法解决循环引用问题(两个对象互相引用,即使都不再被外部使用,引用数也永远不为 0)。
2. 标记清除法(现代 JS 引擎主流算法)
这是目前 Chrome/V8、Node.js 等环境的核心算法,解决了循环引用问题。
- 标记阶段:从根对象出发,遍历所有可访问的对象,给这些'可达'的对象打上标记。
- 清除阶段:清理所有未被标记的对象(即'不可达'的对象),释放其内存。
3. 分代回收(V8 优化策略)
- 新生代(Young Generation):存放短期存活的对象,采用 Scavenge 算法(复制存活对象到 To 区,清空 From 区)。
- 老生代(Old Generation):存放长期存活的对象,主要用标记 - 清除 + 标记 - 整理算法。
闭包为什么不会被销毁
JavaScript 有自动垃圾回收机制,核心判断逻辑是:如果一个对象 / 变量没有任何可达的引用,就会被标记为可回收。
闭包不会被销毁,本质是打破了这个规则 —— 闭包让内部函数保留了对外部函数作用域的引用,导致外部函数的作用域始终有'可达引用',因此不会被回收。
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
const fn = outer();
fn();
fn();
注意:不是所有闭包都不会销毁。如果闭包没有被外部引用,外部函数执行完后,闭包和对应的作用域会被正常回收。合理使用闭包不会造成内存泄漏,只有滥用才会。
什么是内存泄漏?如何排查?
内存泄露是指在程序运行过程中,未能释放不再使用的内存空间,导致内存资源被浪费。
- 使用内存分析工具:Chrome DevTools Memory 面板,或结合 setInterval 使用 console.memory。
- 代码审查:检查是否有未释放的事件监听器、定时器、全局变量。
- 性能监控:观察内存使用情况是否有持续增长的趋势。
- 意外的全局变量:忘记使用 var/let/const 声明。
- 闭包:闭包中引用了不再需要的外部变量。
- 未清理的 DOM 引用:删除 DOM 元素时,未能清理相关的事件监听器。
- 定时器和回调:未能清理不再需要的 setInterval 或 setTimeout。
冯·诺依曼架构是什么?
- 输入设备:键盘、鼠标、摄像头等。
- 输出设备:显示器、打印机、扬声器等。
- 存储器:分为内部存储器(内存/主存储器)和外部存储器(硬盘、U 盘等)。内存又分为 RAM(随机存取,断电丢失)和 ROM(只读,断电不丢)。
- 运算器:算术逻辑单元(ALU),负责执行算术和逻辑运算。
- 控制器:控制整个计算机系统的工作流程,包括指令执行顺序,通常与运算器集成在一起成为 CPU。
计算机内部为何使用二进制?
- 硬件实现简单:只需要两个状态(电压高低),电路设计简单可靠。
- 抗干扰能力强:明显的电压差区分 0 和 1,对噪声容忍度高。
- 逻辑运算方便:与布尔代数契合,简化设计。
- 存储和处理效率高:避免了其他进制转换带来的复杂性。
什么是虚拟内存,为何要使用?
- 让每个程序都以为自己独占一大片连续的内存地址。
- 实际物理内存很小,系统把这些'虚拟地址'映射到真实的物理内存或硬盘上。
- 让程序不用管物理内存多大:兼容性强。
- 让内存'看起来更大':物理内存不够时,用硬盘当'临时内存'。
- 让每个程序互相隔离、更安全:一个程序崩溃不会搞崩整个系统。
- 解决'内存碎片'问题:虚拟内存永远是连续的。
Unicode 编码与 UTF-8
- Unicode:俗称万国码,为每个字符提供唯一数字标识(码点),解决了 ASCII 编码局限性。
- UTF-8:Unicode 最常用的编码方案,变长编码。ASCII 字符用 1 个字节,兼容 ASCII。
- GBK:用于中文字符的编码标准,扩展了 GB2312,但不支持 Unicode 全集。
OSI 模型简述
- 应用层:HTTP、HTTPS、FTP、DNS。
- 表示层:数据格式转换、加密、压缩。
- 会话层:建立、管理、终止会话连接。
- 传输层:TCP、UDP,负责端到端传输、流量控制。
- 网络层:IP、ICMP,负责寻址、路由选择。
- 数据链路层:交换机、网卡,组成帧,差错检测。
- 物理层:网线、光纤,传输比特流。
域名与 IP 的关系
- 一个域名 → 多个 IP:常见,用于负载均衡、CDN。DNS 会返回其中一个。
- 一个 IP → 多个域名:更常见,叫虚拟主机。浏览器请求时带上 Host 头,服务器知道你要访问哪个站。
- 一对一:极少数场景,如独立服务器、独享 IP。
TCP、UDP 和 HTTP 协议的区别
- TCP/UDP:传输层协议,负责数据怎么传。
- HTTP:应用层协议,基于 TCP,负责数据是什么、要干嘛。
- TCP:可靠、面向连接、有拥塞控制、速度慢、开销大。适用于网页浏览、文件传输、支付等要求不丢包的场景。
- UDP:不可靠、无连接、速度快、开销小。适用于直播、游戏、DNS 查询等要求实时的场景。
HTTP:一问一答模式,定义了请求方法、头、状态码、报文格式。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online