前言
我是个工作了快 4 年的 Node.js 工程师,说实话 Node.js 对我挺有缘分的,这个故事又要从 2020 年的暑假开始说起,一晃五年过去了。但是其实我从两年前开始就非常迷茫,觉得自己是应该继续往后端发展呢,还是往前端发展,毕竟 JavaScript 是一个前端语言,并且随着 AI 时代的到来,全栈工程师应该是未来的趋势,因此我对我过去的四年做一个项目以及知识的总结。
应该所具备的能力
前端
Node.js
事件循环
提起 Node.js ,我觉得最应该关注的就是它的事件循环机制。因为它实现了 如何在单线程中实现 高并发、高吞吐量、非阻塞的异步编程模型? 换句话说,它解决了: 在只有一个线程的前提下,如何让程序同时处理多个任务(特别是 I/O 操作)而不会阻塞其他任务的执行。
而 Node.js 的事件循环机制做了什么?
👉 1. 异步 I/O
把“慢”的任务(如文件读写、网络请求)交给底层(libuv / 线程池 / 操作系统)。
当前线程继续执行其他任务,不等待慢任务的返回结果。
👉 2. 事件注册 + 回调机制
慢任务完成后,将对应的“结果处理函数”注册到事件队列中,等主线程空闲了再回调执行。
本质上是“先做别的事,完成了再告诉我”。
👉 3. 事件循环调度机制
主线程通过事件循环,在各个阶段依次从队列中取出任务执行。
保证了 任务之间的有序性 和 系统整体响应能力。
执行阶段
事件循环按阶段执行,每个阶段都有一个 FIFO 队列 存放回调任务:
┌───────────────────────────────┐
│ timers │ -> setTimeout/setInterval
├───────────────────────────────┤
│ pending callbacks │ -> 某些系统操作的回调
├───────────────────────────────┤
│ idle, prepare │ -> libuv内部
├───────────────────────────────┤
│ poll │ -> I/O事件回调执行
├───────────────────────────────┤
│ check │ -> setImmediate
├───────────────────────────────┤
│ close callbacks │ -> 如 socket.on('close')
└───────────────────────────────┘
- timers:执行由
setTimeout()和setInterval()安排的回调。 - pending callbacks:执行某些系统操作(如 TCP 错误类型)的回调。
- idle, prepare:libuv 内部使用,无需关注。
- poll:
- 处理 I/O 事件。
- 如果无 I/O、队列为空,进入 check 阶段。
- 如果 I/O 队列不为空,执行回调。
- check: 执行
setImmediate()注册的回调。 - close callbacks: 处理如
socket.destroy()的关闭事件。
微任务机制(Microtasks)
在每个 事件循环阶段结束前,Node.js 会处理两类微任务队列:
process.nextTick()队列(优先级更高)Promise.then/catch/finally的微任务队列(基于 V8 的微任务)执行顺序:
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
输出顺序:
nextTick
promise
如何还想延伸更多细节:事件循环 (例如事件循环有什么特点?事件循环有什么缺点)
垃圾回收
React 框架
过去面试几家大厂前端工程师岗位,我发现面试官特别喜欢问你使用的是 React 哪个版本。我当时非常不理解为什么都这么喜欢 React 的版本。我记得我在 2022 年写 React 的时候,已经 useEffect 写法了,我的当初使用的是 React 16 版本,具体不知道是 16.x 多少了。在我是校招生的时候,我看前端面经,很喜欢问 React 的生命周期是什么,例如 :componentDidMount()、shouldComponentUpdate()等一些钩子。但是当时并没有使用到,取而待之的就是各种 useXxx()这样的 hook 方法。
Hooks
React 是在 16.8 版本 中正式引入了 Hooks, 这个版本发布的时间是:2019 年 2 月 6 日。
老版本 React 的生命周期:🧬React 生命周期(Class 组件)
对比常见 Hook:🪝 Hooks 阶段(函数组件)
我的理解: Hooks 把生命周期“函数化”了,粒度更细、更灵活
那如何实现自定义的 Hook? 我们说的 “自定义 Hook”(custom hook)是 React 中对函数的一种特殊命名方式,它来源于 React Hook 的使用规范与代码复用的需求。
“自定义 Hook”这个名字的来源,是因为你自己定义了一个使用其他 Hook 的函数。React 官方规定:
“只要函数名以
use开头,并在其内部调用了其他 Hook(比如useState、useEffect),它就是一个自定义 Hook。”
它不是 React 内置的,而是你自己“定义”的 Hook 逻辑。
自定义 Hook 的意义是什么?
- 抽离组件逻辑(例如:多个组件都需要定时器、滚动监听等功能)。
- 复用性高(不用重复粘贴相同的
useEffect/useState逻辑)。 - 可组合性强(多个 Hook 组合使用,逻辑清晰)。
- 让组件更干净、更关注 UI 展示。
如何定义一个自定义 Hook?
定义 Hook 本质上就是一个普通的 JS 函数,但必须:
- 以
use开头 - 可以使用其他 Hook(如
useState、useEffect)
几个示例:
- 封装一个监听窗口大小的 Hook :
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 使用它
function MyComponent() {
const { width, height } = useWindowSize();
return (
<div>
当前窗口尺寸:{width} x {height}
</div>
);
}
- 防抖 Hook(如搜索输入) :
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
const [text, setText] = useState('');
const debouncedText = useDebounce(text, 500);
// 用 debouncedText 去触发接口请求
function SearchComponent() {
const [text, setText] = useState('');
const debouncedText = useDebounce(text, 500);
useEffect(() => {
if (debouncedText) {
// 在这里调用接口
fetchData(debouncedText);
}
}, [debouncedText]);
const fetchData = async (query) => {
try {
const response = await fetch(`https://api.example.com/search?query=${query}`);
const data = await response.json();
console.log(data);
// 处理获取到的数据
} catch (error) {
console.error('请求失败:', error);
}
};
return (
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入搜索内容..."
/>
);
}
useFetch:封装异步请求
import { useEffect, useState } from 'react';
export function useFetch<T = any>(url: string, options?: RequestInit) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!url) return;
setLoading(true);
fetch(url, options)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
const { data, loading, error } = useFetch('/api/user');
Fiber
React Fiber 是在 React 16 中正式发布的。那么为什么 React 会引入 Fiber?
没有 Fiber 的 React 时期的主要性能问题
在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。
针对这一个问题,React 团队从框架层面对 web 页面的运行机制做了优化,就是我们所说的 Fiber,得到很好的效果。
React 的 render 初衷是:
将声明式 UI(JSX/ReactElement)映射成实际的 DOM 操作,并在状态变化时只进行最小化的 DOM 更新。
目标是提供一个:
- 高效(相比手动操作 DOM)
- 声明式(让开发者只关注“UI 长什么样”)
- 自动 diff(避免不必要的 DOM 操作) 的 UI 渲染机制。
🧱 React 15 及以前版本:没有 Fiber 时的渲染逻辑(Stack Reconciler)
旧版本是 递归式同步遍历更新整棵组件树,核心逻辑过程如下:
🔂 1. 初始化:JSX 转换成 ReactElement
ReactDOM.render(<App />, document.getElementById('root'));
- JSX 会被 Babel 转换成
React.createElement(type, props, children) - 最终形成一个嵌套结构的 ReactElement 树(描述 UI 的数据结构)
🌳 2. 构建组件实例树 + 生成 DOM(初次挂载)
- 递归遍历 ReactElement:
- 如果是函数组件:调用函数,拿到子元素;
- 如果是类组件:实例化类,调用
render(),拿到子元素; - 如果是原生标签:直接生成对应 DOM 节点;
- 然后递归地挂载子节点,最终把整棵 DOM 插入页面。
这个过程类似:
function mount(element) {
if (typeof element.type === 'function') {
const childElement = element.type(element.props);
return mount(childElement);
} else {
const dom = document.createElement(element.type);
// 设置属性 + 递归 appendChild
return dom;
}
}
🔁 3. 组件更新(状态或 props 变化时)
当组件调用 setState() 时:
- React 会从该组件重新开始向下递归对比旧的 ReactElement 树和新的 ReactElement 树
- 对比逻辑是通过
reconciler.diff()实现的:- tag 相同、key 相同 → 复用节点,更新属性
- 否则认为是新的节点 → 卸载旧的、挂载新的
- 最终生成最小化的 DOM 操作 patch 应用到页面上
这是经典的 stack reconciler 实现,即:
使用函数递归调用栈完成协调(reconciliation)过程
✅ 总结一下
旧版本 React render 的目的:
- 将 JSX → 虚拟 DOM(ReactElement)→ 真 DOM
- 最小化 DOM 操作(diff 算法)
- 提供 declarative(声明式) UI 渲染模型
实现方式(React 15 及以前):
- 使用递归同步遍历整个组件树
- 不能中断渲染过程
- 使用 Stack Reconciler 栈调和
- 性能和交互体验受限
Fiber 的出现是为了解决:
- 渲染中断性
- 高优先级任务调度
- 并发渲染能力
什么是 Fiber
Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:
Fiber 是一个执行单元,每次执行完一个执行单元,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去。
Fiber 关键特性:
- 增量渲染
- Fiber 把一次完整的渲染拆成“小任务”,均匀到每一帧里面去执行。
- 暂停,终止,复用渲染任务
- 不同更新的优先级
- 并发方面新的基础能力
React Fiber 运行流程图
React Fiber 渲染调度流程图
- 🔁 渲染阶段(render phase):构建 Fiber 树,可以被中断。
- 🚦 提交阶段(commit phase):更新 DOM,不可中断。
- 🧠 增量渲染的核心是 “每执行一段,看是否有更高优先级任务”,用 requestIdleCallback、MessageChannel 或 scheduler 调度控制。
React Fiber 节点结构示意图
🧠 结构说明
child:指向第一个子节点sibling:指向下一个兄弟节点(链表形式遍历子节点)return:指向父节点(方便回溯)alternate:指向当前节点的“另一个版本”(双缓存结构:currentvsworkInProgress)
这个结构让 React 在渲染时可以:
- 遍历整个组件树
- 构建新的 Fiber 树同时保留旧的
- 快速对比差异并提交最小更新
🧱 FiberNode 数据结构(简化版源码)
type FiberTag = number; // FunctionComponent, ClassComponent, HostComponent 等
type Flags = number; // 用于表示副作用,比如 Placement, Update
class FiberNode {
// 基础标识
tag: FiberTag;
key: null | string;
type: any;
elementType: any;
stateNode: any;
// 构建树结构(核心三叉链表结构)
return: FiberNode | null; // 父节点
child: FiberNode | null; // 第一个子节点
sibling: FiberNode | null; // 下一个兄弟节点
index: number;
// 双缓存机制
alternate: FiberNode | null; // current 与 workInProgress 相互指向
// 状态相关
pendingProps: any; // 本次更新传入的 props
memoizedProps: any; // 上次渲染完的 props
memoizedState: any; // 上次渲染完的 state
updateQueue: any; // 更新队列(setState 就存在这里)
// 副作用标志
flags: Flags;
subtreeFlags: Flags;
// DOM 或组件引用
ref: null | (((handle: any) => void) | { current: any });
constructor(tag, pendingProps, key) {
this.tag = tag;
this.key = key;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.memoizedState = null;
this.updateQueue = null;
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.stateNode = null;
this.type = null;
this.elementType = null;
this.alternate = null;
this.flags = 0;
this.subtreeFlags = 0;
this.ref = null;
}
}
Next.js 框架 - SSR
Next.js 中的 SSR、SSG、ISR、CSR 有什么区别?你在项目中如何选择?
✅ SSR(Server-side Rendering):
- 页面在请求时在服务端生成 HTML
- 实现方式:
getServerSideProps - 优点:数据实时,适合登录态/搜索页
- 缺点:每次请求都会触发渲染,性能开销大
✅ SSG(Static Site Generation):
- 页面在构建时就静态生成
- 实现方式:
getStaticProps - 优点:性能极高,适合博客/产品页
- 缺点:构建时固定数据,缺乏实时性
✅ ISR(Incremental Static Regeneration):
- 静态生成 + 增量更新,结合了 SSR 和 SSG 的优点
getStaticProps + revalidate- 优点:支持缓存+定时刷新,兼顾性能与实时性
- 典型场景:内容平台、新闻网站
✅ CSR(Client-side Rendering):
- 在浏览器中用 JS 获取并渲染数据
- 实现方式:
useEffect中调用fetch - 优点:无需服务端,交互灵活
- 缺点:不利于 SEO,首屏渲染慢
三、使用选择建议(结合场景):
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 详情页 / 营销页 | SSG 或 ISR | 静态渲染快,SEO 友好 |
| 登录态页面(如个人中心) | SSR | 需要实时性与权限判断 |
| 列表页 / 搜索页 | SSR 或 CSR | 数据实时变化 |
| 新闻类平台 / 内容 CMS | ISR | 可设置定期重建页面 |
| SPA 应用 / 后台系统 | CSR | 不需要 SEO,操作性强 |
SSR 服务端渲染的底层逻辑
Next.js 实现服务端渲染(SSR, Server-Side Rendering)的核心原理,是在 Node.js 环境中运行 React 组件生成 HTML,再把生成的 HTML 返回给客户端,从而实现首屏加速与 SEO 友好等效果。下面我们来深入解析一下底层的实现原理:
整体流程
- 浏览器请求页面(如 /about)
- Next.js 路由系统匹配到对应的页面组件(如
**pages/about.tsx**) - 在服务器端调用
**getServerSideProps**(如果存在)获取数据 - 使用 React 的服务端渲染 API(如
**renderToString**或**renderToPipeableStream**)将组件转为 HTML 字符串 - 将 HTML 返回给浏览器,浏览器解析 HTML 并加载相关 JS
- 前端进行 hydration (注水),将静态 HTML 变为可交互的 React 应用
相对于 SSG 不同的方式
| 函数名 | 执行频率 | 执行时机 | 运行环境 | 典型用途 |
|---|---|---|---|---|
**getServerSideProps** | 每次请求都会执行 | 请求页面时 | 服务端 | 登录后页面、权限控制、实时内容 |
**getStaticProps** | 只在构建时执行一次 | **next build** | 服务端 | 博客/产品页等静态内容 |
🌟 Next.js 构建流程(next build)
当执行 next build 时,整个构建流程大致可以拆解为以下几个阶段:
- 初始化阶段(Preflight)
- 读取配置文件(
next.config.js) - 检查必要文件是否存在(如
pages/_app.js、pages/index.js) - 分析 TypeScript 配置(如使用
.ts/.tsx文件) - 初始化 Babel 和 Webpack 配置
- 加载插件和扩展(如
next-transpile-modules)
- 编译阶段(Build)
📄 页面编译
- 使用 Webpack + Babel 对
pages/、app/、components/目录的文件进行打包编译 - 根据
getStaticProps/getServerSideProps判断页面渲染方式(SSG、SSR、ISR)
🚀 编译结果
- 输出服务端代码(
server) - 输出客户端代码(
static/chunks/pages) - 使用
react-server-dom-webpack编译 React Server Components(仅适用于 App Router)
- 优化阶段(优化输出产物)
- 静态资源优化(例如 CSS、JS 分包,Tree-Shaking,压缩)
- Image 优化(预生成优化版本)
- 预渲染(Pre-render)
- SSG 页面会被渲染为 HTML + JSON
- ISR 页面会生成静态 HTML,但带有 revalidate 配置
- SSR 页面只保留入口模块,运行时动态渲染
- 产物生成阶段(Output)
- 输出
.next目录,包含以下内容:.next/static:静态资源.next/server:服务端 SSR 渲染代码.next/cache:缓存数据.next/build-manifest.json、.next/prerender-manifest.json:构建元信息
- 分析阶段(可选)
- 使用
next build && next export或next build && next analyze查看构建分析报告 @next/bundle-analyzer插件可用于可视化 bundle 大小
Webpack 工程化
核心原理与机制
- Webpack 构建流程原理
- 流程六阶段:
- 初始化参数(配置文件解析)
- 创建 Compiler 对象
- 调用 run 启动编译
- 确定入口(entry)
- 递归编译模块(loader 处理,构建 AST)
- 输出 assets(plugins 修改最终资源)
实战应用:自定义构建插件,比如统计构建耗时、输出构建依赖图。
- Module Resolution 模块解析机制
- 默认查找路径:
node_modules - 扩展字段:
resolve.extensions,resolve.alias,resolve.mainFields - 支持三种模块:
- CommonJS(require)
- ES Module(import/export)
- AMD(较少用)
实战应用:通过
alias优化业务代码模块路径引用、支持 monorepo 结构。
- Loader 机制
- 作用:模块预处理,转化文件内容(如 .ts -> .js)
- 执行顺序:从右到左(或从下到上)
- Loader 分类:
preLoaders(前置处理器)normal loaderspostLoaders(后置处理器)
实战应用:编写自定义 loader,例如 markdown 转 html、组件按需加载。
- Plugin 机制
- 作用:用于构建过程中扩展 webpack 功能(钩子系统)
- 插件通过
compiler.hooks或compilation.hooks插入处理逻辑
实战应用:打包进度条(使用
ProgressPlugin)、自定义插件实现构建时间日志、资源输出分析
性能优化策略
- Tree Shaking 原理
- 依赖 ESModule 的静态结构特性(import/export)
- 只移除未被使用的 export 成员
- 配合:
sideEffects: false(package.json)usedExports: true(webpack 配置)
实战应用:减少无效代码输出,压缩包体积
- Code Splitting(代码分割)
- 实现方式:
- 入口分割(多个 entry)
- 动态导入:
import() SplitChunksPlugin(提取公共代码)
实战应用:拆分页面路由模块,首屏加载更快、将
react,lodash等大包分离成独立 chunk,利用缓存
- 缓存优化
contenthash: 文件变更才更新文件名cache-loader,babel-loader的缓存- 持久化缓存:
cache: { type: 'filesystem' }
实战应用:提高二次构建速度,适用于大型项目多模块打包
4.并行与多进程加速
- 使用
thread-loader、parallel-webpack - 开启
TerserPlugin多线程压缩:parallel: true - 文件缓存配合使用
实战应用:优化构建耗时,CI/CD 构建显著提速
实战配置能力(可组合 + 可扩展)
- 多环境配置(重要)- 可用于灰度发布
- 使用
webpack-merge合并基础配置 + dev/prod 配置 - 动态注入环境变量(
DefinePlugin)
new webpack.DefinePlugin({
'process.env.API_BASE': JSON.stringify(process.env.API_BASE),
});
- 开发体验提升
- 热更新(HMR)原理:webpack-dev-server + HMR 插件
- 使用
SourceMap定位错误(devtool 配置详解) - 使用
eslint-loader+stylelint保证代码规范
- 项目架构支持
- 多页面应用支持(MPA)
- 微前端支持(Module Federation)
- SSR 场景优化(可用于自研 SSR 框架或优化 Next.js)