一、React (T0)
1.1 hooks原理
题目:手写实现一个简化版 React Hooks(useState + useEffect)
请实现一个简化版的 React Hooks 系统,包含 useState 和 useEffect,能够支持以下功能:
jsx
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('counter');
useEffect(() => {
console.log('count changed:', count);
return () => console.log('cleanup:', count);
}, [count]);
return (
<div>
<h1>{name}: {count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setName(name === 'counter' ? 'cnt' : 'counter')}>toggle</button>
</div>
);
}
参考答案
javascript
let currentComponent = null;
let hookIndex = 0;
class ReactElement {
constructor(type, props) {
this.type = type;
this.props = props;
}
}
function useState(initialValue) {
const hooks = currentComponent.hooks;
const index = hookIndex;
if (hooks[index] === undefined) {
hooks[index] = {
state: typeof initialValue === 'function' ? initialValue() : initialValue,
queue: []
};
}
const currentState = hooks[index].state;
const setState = (newState) => {
const value = typeof newState === 'function' ? newState(currentState) : newState;
if (value !== currentState) {
hooks[index].state = value;
render();
}
};
hookIndex++;
return [currentState, setState];
}
function useEffect(callback, deps) {
const hooks = currentComponent.hooks;
const index = hookIndex;
const prevDeps = hooks[index]?.deps;
const hasChanged = !prevDeps || deps.some((dep, i) => dep !== prevDeps[i]);
if (hasChanged) {
if (hooks[index]?.cleanup) {
hooks[index].cleanup();
}
hooks[index] = {
deps,
cleanup: callback()
};
}
hookIndex++;
}
function createElement(type, props, ...children) {
return new ReactElement(type, { ...props, children });
}
function render() {
hookIndex = 0;
const element = currentComponent();
console.log('Rendered:', JSON.stringify(element));
}
function mount(Component) {
currentComponent = Component;
currentComponent.hooks = [];
render();
}
const MyCounter = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('counter');
useEffect(() => {
console.log('count changed:', count);
return () => console.log('cleanup:', count);
}, [count]);
return createElement('div', null,
createElement('h1', null, `${name}: ${count}`),
createElement('button', { onClick: () => setCount(count + 1) }, '+'),
createElement('button', { onClick: () => setName(name === 'counter' ? 'cnt' : 'counter') }, 'toggle')
);
};
mount(MyCounter);
详细解析
核心原理:
- 链表存储 Hooks 状态:React 使用链表存储每个组件的所有 hooks 状态,每次 render 时按顺序读取
- Fiber 节点关联 :每个 ReactElement/Fiber 节点通过
memoizedState属性指向 hooks 链表 - 下标递增 :每次
useState或useEffect调用时,递增全局下标来获取对应状态 - deps 比较 :
useEffect通过浅比较 deps 数组判断是否需要重新执行 - cleanup 函数:effect 执行后返回的函数会在下次 effect 执行前或组件卸载时调用
关键点:
- Hooks 不能在条件语句、循环中调用,因为依赖下标顺序
useState的函数式更新确保基于最新状态计算- Effect 的 cleanup 模式用于防止内存泄漏和竞态条件
1.2 diff算法
题目:React 的 Diff 算法是如何工作的?为什么 React 选择 O(n³) 复杂度的树 diff 却采用了 O(n) 的策略?
参考答案
React 采用的 O(n) 策略是"分层比较":
- Tree Diff:同层节点比较,不同则删除重建
- Component Diff:同类型组件继续 diff 子节点
- Element Diff:同层级元素比较 key 进行位置复用
React 选择 O(n) 的原因:
| 策略 | 复杂度 | 实际效果 |
|---|---|---|
| 传统树 Diff | O(n³) | 最小编辑距离,计算代价高 |
| React 分层 Diff | O(n) | 实际场景效果接近最优 |
为什么可行:
- Web UI 中 DOM 节点跨层级的操作极少(<1%)
- 组件更新通常是同层兄弟节点的增删
- 实际业务中跨层级移动 DOM 成本高,用户体验差
图示:
less
Layer 1: A A
Layer 2: B C → B C
Layer 3: D E D E F (新)
操作: F 是新节点,只需创建插入
D, E 节点位置不变,通过 key 复用
详细解析
Tree Diff 核心规则:
- 根节点类型不同 → 销毁旧树,创建新树
- 根节点类型相同 → 复用 DOM 节点,只更新属性
Component Diff 优化:
jsx
// 组件类型改变 → 整个组件替换
// 类型相同但 props 相同 → 跳过更新
// 这就是 shouldComponentUpdate 存在的意义
Element Diff 的 key 机制:
jsx
// 无 key:位置改变 = 删除 + 重建
// 有 key:位置改变 = 移动
{list.map(item => <div key={item.id}>{item.name}</div>)}
性能优化建议:
- 保持 DOM 结构稳定,避免跨层级移动
- 使用稳定的唯一 key(如 id,避免 index)
- 在列表末尾添加元素性能最优
- 在列表头部插入元素会导致所有节点重建
1.3 React18并发
题目:什么是 React 18 的并发渲染(Concurrent Rendering)?它解决了什么问题?请举例说明 useTransition 的使用场景。
参考答案
并发渲染核心概念:
css
普通模式:render 是同步阻塞的
┌─────────────┐
Input │ 完整渲染 │ → UI 卡顿
└─────────────┘
并发模式:render 可中断
┌──────────┬──────────┬──┐
Input │ 优先级高 │ 优先级高 │ │ 低优先级任务
│ 渲染完成 │ 渲染完成 │...│ 后台慢慢完成
└──────────┴──────────┴──┘
→ UI 保持响应
解决的问题:
- 渲染可中断:长时间渲染任务可以分片执行
- 状态更新优先级 :
startTransition标记非紧急更新 - Suspense 边界:数据未就绪时显示 loading
- 自动批处理:所有更新统一批处理,减少 render 次数
useTransition 使用示例:
jsx
import { useState, useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(e) {
setQuery(e.target.value);
// 标记为非紧急更新,可以被高优先级更新打断
startTransition(() => {
// 模拟大量数据处理
const newResults = expensiveSearch(e.target.value);
setResults(newResults);
});
}
return (
<div>
<input value={query} onChange={handleSearch} />
{/* 高优先级更新不受影响 */}
<div className="suggestions">{/* 即时显示的联想词 */}</div>
{/* 低优先级更新显示加载状态 */}
<div style={{ opacity: isPending ? 0.5 : 1 }}>
{results.map(r => <ResultItem key={r.id} data={r} />)}
</div>
</div>
);
}
详细解析
并发渲染三大支柱:
- Fiber 架构:将渲染工作拆分成可中断的小单元
- Scheduler:基于优先级队列的任务调度
- ** lanes/优先级机制**:区分更新优先级
useTransition 原理:
- 将闭包内的 setState 标记为
transition优先级 - 期间产生的更高优先级更新可以打断当前渲染
isPending告知用户有后台任务进行中
实际应用场景:
- 搜索框输入 → 实时搜索结果(高优先级)
- 搜索触发的大数据列表渲染(低优先级,用 transition)
- Tab 切换 → 新页面内容(transition)
- Modal 弹窗 → 立即响应(高优先级)
React 18 自动批处理增强:
jsx
// React 18 之前:setTimeout 中的 setState 会触发多次 render
// React 18 之后:自动批处理,只触发一次 render
setTimeout(() => {
setA(1);
setB(2);
// React 18 只触发一次 render
}, 0);
1.4 状态管理
题目:现代 React 应用中,状态管理方案有哪些?请对比 Context、Zustand、Redux Toolkit 的适用场景,并说明在 AI Agent 产品中如何选择。
参考答案
状态管理方案对比:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Context + useReducer | 中小型应用、主题/语言/用户态 | 原生、无依赖 | 重渲染问题、层级深时性能差 |
| Zustand | 中型应用、简单状态共享 | 轻量、API 简洁、immer 支持 | 缺少强制规范 |
| Redux Toolkit | 大型复杂应用、需要严格规范 | 生态成熟、devtools 强大 | 配置复杂、模板代码多 |
| Jotai | 原子化状态、细粒度更新 | 按需渲染、组合性好 | 概念新、学习成本 |
| TanStack Query | 服务端状态、缓存 | 自动缓存/同步/乐观更新 | 定位不同 |
代码示例 - Zustand:
jsx
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
const useAIStore = create(
immer((set) => ({
// AI 对话状态
messages: [],
isLoading: false,
conversationId: null,
// Actions
addMessage: (role, content) => set((state) => {
state.messages.push({ role, content, id: crypto.randomUUID(), timestamp: Date.now() });
}),
setLoading: (loading) => set({ isLoading: loading }),
clearMessages: () => set({ messages: [], conversationId: null }),
}))
);
// 在组件中使用
function ChatPanel() {
const { messages, isLoading, addMessage } = useAIStore();
const sendMessage = async (content) => {
addMessage('user', content);
// 调用 AI API...
};
return (
<div>
{messages.map(msg => (
<MessageBubble key={msg.id} role={msg.role} content={msg.content} />
))}
</div>
);
}
AI Agent 产品选型建议:
┌─────────────────────────────────────────────────┐
│ 状态类型分类 │
├─────────────────┬───────────────────────────────┤
│ UI 本地状态 │ useState / useReducer │
│ 跨组件共享 UI │ Context / Zustand │
│ 服务端缓存状态 │ TanStack Query / SWR │
│ 复杂 AI 会话状态 │ Zustand + Immer │
│ 全局配置/主题 │ Context │
└─────────────────┴───────────────────────────────┘
详细解析
Context 的重渲染问题:
jsx
// 问题:Provider 下的所有组件都会重渲染
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
// 解决方案 1:拆分 Context
<ThemeProvider> // 只影响需要主题的组件
<LocaleProvider> // 只影响需要语言的组件
<AuthProvider> // 只影响需要登录态的组件
// 解决方案 2:使用 useMemo 选择性订阅
const theme = useContext(ThemeContext); // 整个 context
const { color } = useContext(ThemeContext); // 只订阅需要的值(仍然会重渲染)
Redux Toolkit vs Zustand:
- Redux Toolkit:需要严格规范、大型团队、复杂业务逻辑
- Zustand:快速开发、简单状态、需要性能优化
AI Agent 场景特殊考虑:
- 对话历史:大数组状态,需要注意更新性能
- 流式响应:需要支持增量更新 messages
- 多会话管理:Tab 页签隔离,每个 tab 独立 store 实例
- 持久化:对话内容需要 localStorage/indexedDB 持久化
二、JavaScript/TypeScript (T1)
2.1 event loop
题目:请详细描述 JavaScript 事件循环机制,并分析以下代码的执行顺序:
javascript
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
Promise.resolve().then(() => {
setTimeout(() => console.log('4'), 0);
console.log('5');
});
console.log('6');
// 问:最终输出顺序是什么?
参考答案
最终输出顺序 : 1 → 6 → 3 → 5 → 2 → 4
执行过程详解:
scss
┌─────────────────────────────────────────────────────────────┐
│ 执行栈 (Call Stack) │
│ 同步代码按顺序执行 → 输出 1 → 输出 6 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 微任务队列 (Microtask Queue) │
│ Promise.then 回调 │
│ queueMicrotask │
│ MutationObserver │
│ │
│ 处理顺序:先入先出,直至队列为空 │
│ → 输出 3 │
│ → 执行 5 的同步代码,输出 5 │
│ → setTimeout(4) 进入宏任务队列 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 宏任务队列 (Macrotask Queue) │
│ setTimeout 回调 │
│ setInterval 回调 │
│ I/O 操作 │
│ UI 渲染 (浏览器) │
│ │
│ 处理完一个宏任务 → 检查微任务队列 → 渲染 → 下一个宏任务 │
│ → 输出 2 │
│ → 输出 4 │
└─────────────────────────────────────────────────────────────┘
详细解析
关键概念:
- 执行栈:同步代码立即执行,LIFO 顺序
- 微任务队列 :每个宏任务执行完后、在渲染前,清空所有微任务
- 宏任务队列:每执行完一个,才检查微任务
常见微任务 vs 宏任务:
| 微任务 (Microtasks) | 宏任务 (Macrotasks) |
|---|---|
| Promise.then/catch/finally | setTimeout / setInterval |
| queueMicrotask | setImmediate (Node.js) |
| MutationObserver | I/O 操作 |
| async/await (底层是 Promise) | requestAnimationFrame |
| process.nextTick (Node.js) | UI 渲染 (浏览器) |
async/await 原理:
javascript
// 写法
async function foo() {
await bar();
console.log('after await');
}
// 等价于
function foo() {
return Promise.resolve().then(() => {
return bar().then(() => {
console.log('after await');
});
});
}
浏览器渲染时机:
javascript
setTimeout(() => console.log('setTimeout'), 0);
// 渲染发生在宏任务之间
requestAnimationFrame(() => console.log('rAF'));
// rAF 在下一次渲染前执行
2.2 promise
题目:实现一个 Promise.allSettled 方法,并说明它与 Promise.all 的区别。
参考答案
javascript
/**
* Promise.allSettled - 返回所有 Promise 的结果,无论成功或失败
* @param {Promise[]} promises - Promise 数组
* @returns {Promise<Array>} 所有 Promise 的 settled 结果
*/
function promiseAllSettled(promises) {
return Promise.all(
promises.map((promise) =>
Promise.resolve(promise).then(
(value) => ({
status: 'fulfilled',
value
}),
(reason) => ({
status: 'rejected',
reason
})
)
)
);
}
// 使用示例
const promises = [
Promise.resolve(1),
Promise.reject(new Error('error')),
Promise.resolve(3)
];
promiseAllSettled(promises).then((results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error: error },
// { status: 'fulfilled', value: 3 }
// ]
});
Promise.all vs Promise.allSettled 对比:
| 特性 | Promise.all | Promise.allSettled |
|---|---|---|
| 成功条件 | 所有都成功 | 无条件,始终成功 |
| 失败行为 | 任一失败则整体失败 | 每个独立记录成功/失败 |
| 返回结果 | 只保留成功值 | 每个 promise 的状态+值 |
| 典型场景 | "全部成功才继续" | "收集所有结果,容错处理" |
javascript
// Promise.all:任一失败全部失败
const results = await Promise.all([
fetchUser(userId),
fetchPermissions(userId),
fetchPreferences(userId)
]);
// 任意一个失败,整个结果丢失
// Promise.allSettled:收集所有结果
const results = await Promise.allSettled([
fetchUser(userId),
fetchPermissions(userId),
fetchPreferences(userId)
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`成功:`, result.value);
} else {
console.log(`失败 ${index}:`, result.reason);
}
});
详细解析
Promise.allSettled 适用场景:
- 批量操作,容错处理:如批量发送通知,失败的不影响其他的
- 并行执行,独立追踪:如并行加载多个资源,部分失败不影响展示
- Dashboard 场景:展示所有数据源的状态,而不是因为一个失败全部不展示
手写 Promise.allSettled 关键点:
javascript
// 1. 先用 Promise.resolve 包装,确保输入是 Promise
Promise.resolve(promise)
// 2. then 接收成功和失败两种情况
.then(
value => ({ status: 'fulfilled', value }),
reason => ({ status: 'rejected', reason })
)
// 3. Promise.all 确保全部处理完才返回
Promise.all(mappedPromises)
Promise 静态方法全家桶:
javascript
Promise.resolve(value) // 返回一个 resolved Promise
Promise.reject(reason) // 返回一个 rejected Promise
Promise.all(iterable) // 全部成功才成功
Promise.allSettled(arr) // 收集所有结果
Promise.race(iterable) // 返回最先 settle 的结果
Promise.any(iterable) // 返回最先 fulfilled 的,忽略 rejections
2.3 闭包
题目:什么是闭包?以下代码会产生什么问题?如何修复?
javascript
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// 期望输出:0, 1, 2, 3, 4
// 实际输出:5, 5, 5, 5, 5
参考答案
闭包定义:
闭包是指函数能够访问其词法作用域外部的变量,即使该函数在其词法作用域之外执行。
javascript
function createCounter() {
let count = 0; // 闭包变量
return function() {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 变量被闭包引用,不会被 GC 回收
问题分析:
css
var i 的作用域是函数级别,不是块级
for 循环结束后,i = 5
setTimeout 回调形成闭包,引用同一个 i
100ms 后执行时,i 已经是 5
→ 输出 5 个 5
三种修复方式:
javascript
// 方式 1:使用 let(块级作用域)
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// let 每次循环创建新作用域,闭包捕获各自的 i
// 方式 2:使用 IIFE 闭包
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// 方式 3:使用 bind 传参
for (var i = 0; i < 5; i++) {
setTimeout(console.log.bind(null, i), 100);
}
详细解析
闭包的应用场景:
javascript
// 1. 数据私有化
function createUser(name) {
return {
getName: () => name,
setName: (newName) => { name = newName; }
};
}
// 2. 函数工厂
function multiplier(factor) {
return (num) => num * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
// 3. 防抖/节流
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 4. 缓存/记忆化
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
闭包的内存泄漏风险:
javascript
// 问题:大型对象被闭包引用,无法被 GC
function badExample() {
const largeData = new Array(1000000);
const button = document.getElementById('button');
button.addEventListener('click', () => {
console.log(largeData.length); // 即使不用 largeData,也被闭包引用
});
}
// 解决:及时清理
function goodExample() {
const largeData = new Array(1000000);
const button = document.getElementById('button');
const handler = () => console.log(largeData.length);
button.addEventListener('click', handler);
// 不用时移除
return () => button.removeEventListener('click', handler);
}
2.4 原型链
题目:请解释 JavaScript 的原型链机制,并实现一个 myInstanceof 方法。
参考答案
原型链图示:
javascript
┌─────────────────────────────────────────────────────────┐
│ Object.prototype │
│ ↑ │
│ / │
│ / │
│ Person.prototype │
│ ↑ │
│ / │
│ / │
│ person 实例 │
│ │
│ person.__proto__ === Person.prototype │
│ Person.prototype.__proto__ === Object.prototype │
│ Object.prototype.__proto__ === null ← 链的终点 │
└─────────────────────────────────────────────────────────┘
原型链查找规则:
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`I'm ${this.name}`);
};
const person = new Person('Alice', 25);
// 属性查找顺序:
// 1. person 自身属性 → name, age
// 2. person.__proto__ (Person.prototype) → sayHello
// 3. person.__proto__.__proto__ (Object.prototype) → hasOwnProperty, toString...
// 4. null → 查找结束
myInstanceof 实现:
javascript
/**
* myInstanceof - 模拟 instanceof 操作符
* @param {any} left - 要检查的对象
* @param {any} right - 要检查的构造函数
* @returns {boolean}
*/
function myInstanceof(left, right) {
// 基础类型直接返回 false
if (left === null || typeof left !== 'object') {
return false;
}
// 获取左侧对象的原型
let proto = Object.getPrototypeOf(left);
// 沿着原型链向上查找
while (proto !== null) {
// 如果找到构造函数的 prototype,返回 true
if (proto === right.prototype) {
return true;
}
// 继续向上查找
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 测试
function Person() {}
const person = new Person();
console.log(myInstanceof(person, Person)); // true
console.log(myInstanceof(person, Object)); // true
console.log(myInstanceof(person, Array)); // false
console.log(myInstanceof('string', String)); // true
console.log(myInstanceof(123, Number)); // true
console.log(myInstanceof(null, Object)); // false
详细解析
ES6 类与原型链:
javascript
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Rex', 'German Shepherd');
// 原型链:
// dog → Dog.prototype → Animal.prototype → Object.prototype → null
原型链继承的实现方式:
javascript
// 原型链继承
function Parent() { this.name = 'parent'; }
function Child() { Parent.call(this); this.type = 'child'; }
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// Class 继承(ES6+,语法糖)
class Parent {
constructor() { this.name = 'parent'; }
}
class Child extends Parent {
constructor() {
super();
this.type = 'child';
}
}
实际开发中的应用:
javascript
// 1. 判断数据类型(原型链方法)
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(null); // "[object Null]"
// 2. 原型方法复用(节省内存)
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('length')); // 从 Object.prototype 继承
// 3. 继承已有对象
const parent = { greet: () => console.log('Hello') };
const child = Object.create(parent);
child.greet(); // "Hello" - 通过原型链访问
三、Next.js (T1)
3.1 App Router
题目:Next.js App Router 与 Pages Router 相比有哪些核心变化?为什么说 App Router 是未来趋势?
参考答案
核心变化对比:
| 特性 | Pages Router | App Router |
|---|---|---|
| 路由约定 | 文件系统路由 | 嵌套文件夹 + layout |
| 组件类型 | Client Component 默认 | Server Component 默认 |
| 数据获取 | getServerSideProps | async 组件 / fetch |
| 布局 | 单一 _app.js | 嵌套 layout.js |
| 路由组 | (group) | (group) |
| 加载状态 | 无内置 | loading.js |
| 错误处理 | _error.js | error.js |
| 样式 | CSS Modules | CSS Modules + CSS-in-JS |
App Router 核心概念:
ini
app/
├── layout.js # 根布局,所有页面共享
├── page.js # 首页 (/)
├── about/
│ ├── page.js # /about
│ └── loading.js # /about 加载状态
├── blog/
│ ├── layout.js # 博客布局(header/footer)
│ ├── page.js # /blog
│ └── [slug]/
│ └── page.js # /blog/:slug
└── (marketing)/
├── layout.js # 营销页面布局
├── page.js # /
└── about/
└── page.js # /about
代码示例:
jsx
// app/blog/[slug]/page.js
// Server Component - 默认,所有数据获取在服务端完成
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
// 静态生成 + ISR
export async function generateStaticParams() {
const posts = await db.post.findMany();
return posts.map((post) => ({
slug: post.slug,
}));
}
// 动态元数据
export async function generateMetadata({ params }) {
const post = await db.post.findUnique({
where: { slug: params.slug }
});
return {
title: post?.title || 'Post not found',
description: post?.excerpt,
};
}
export default async function BlogPost({ params }) {
const post = await db.post.findUnique({
where: { slug: params.slug }
});
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
详细解析
为什么 App Router 是未来:
-
React Server Components (RSC)
- 默认服务端渲染,减少客户端 JS bundle
- 直接在服务端访问数据库/API
- 敏感信息不泄露到客户端
-
嵌套布局系统
jsx// 根布局:导航栏、底部版权 // 子布局:侧边栏 // 页面:具体内容 // 完美契合大型应用的布局需求 -
Streaming + Suspense
jsx// 页面骨架立即可见,内容逐步加载 export default function Page() { return ( <div> <Navbar /> {/* 立即显示 */} <Suspense fallback={<Skeleton />}> <HeavyComponent /> {/* 流式加载 */} </Suspense> </div> ); } -
更精细的数据获取
jsx// 可以针对每个组件独立数据获取 // 不再需要顶层统一获取
迁移建议:
- 新项目直接使用 App Router
- 旧项目渐进式迁移:App Router 与 Pages Router 可以共存
- 注意 Client Component 的边界划分
3.2 SSR
题目:什么是 SSR(服务端渲染)?Next.js App Router 中的 SSR 与传统 SSR 有何不同?请说明 React Server Components 如何优化 SSR 场景。
参考答案
SSR 核心原理:
markdown
传统 CSR:
┌──────┐ ┌─────────┐ ┌─────────────┐
│用户 │ → │服务器返回 │ → │ 加载 JS │
│请求 │ │ HTML │ │ 客户端渲染 │
└──────┘ └─────────┘ └─────────────┘
(空白/loading) (白屏等待)
SSR:
┌──────┐ ┌─────────────┐
│用户 │ → │服务器渲染完成 │
│请求 │ │ 返回完整HTML │
└──────┘ └─────────────┘
(立即可见)
App Router SSR vs 传统 SSR:
| 维度 | 传统 SSR (Pages Router) | App Router SSR |
|---|---|---|
| 渲染主体 | 服务端完整渲染 | React Server Components |
| 组件嵌套 | 全部服务端渲染 | 可选择 Client/Server |
| 水合成本 | 全量水合 | 部分水合(只水合交互部分) |
| 数据获取 | getServerSideProps | async 组件 |
| 状态传递 | 序列化 props | 直接数据库访问 |
React Server Components 优化示例:
jsx
// app/dashboard/page.js
// 整个组件在服务端执行
import { db } from '@/lib/db';
import Sidebar from './Sidebar'; // Server Component
import WeatherWidget from './WeatherWidget'; // Client Component
import HeavyChart from './HeavyChart'; // Server Component
export default async function Dashboard() {
// 直接服务端访问数据库,无需 API 层
const user = await db.user.findUnique({
where: { id: currentUserId }
});
const metrics = await db.metrics.findMany({
where: { userId: user.id }
});
return (
<div className="dashboard">
{/* 1. 服务端组件:直接用数据渲染,无额外 JS */}
<Sidebar user={user} />
{/* 2. 客户端组件:需要交互的才水合 */}
<WeatherWidget city={user.city} />
{/* 3. 服务端组件:大数据图表,服务端生成 */}
<HeavyChart data={metrics} />
</div>
);
}
// app/dashboard/Sidebar.js
// Server Component - 无需 'use client'
export default function Sidebar({ user }) {
// 服务端直接渲染,不发送到客户端
return (
<aside>
<h2>{user.name}</h2>
<nav>{/* 导航链接 */}</nav>
</aside>
);
}
// app/dashboard/WeatherWidget.js
// Client Component - 需要 useState/useEffect
'use client';
import { useState, useEffect } from 'react';
export default function WeatherWidget({ city }) {
const [weather, setWeather] = useState(null);
useEffect(() => {
// 客户端获取天气数据
fetchWeather(city).then(setWeather);
}, [city]);
return <div>{weather?.temp}°C</div>;
}
详细解析
RSC 带来的性能提升:
yaml
Bundle Size 对比:
CSR 模式:
├─ React + ReactDOM: ~45KB
├─ 页面组件: ~200KB
└─ 第三方库: ~150KB
总计: ~395KB
RSC 模式 (App Router):
├─ React + ReactDOM: ~45KB (只水合交互部分)
├─ Server Components: ~0KB (不发送 JS)
├─ Client Components: ~50KB
└─ 第三方库: ~0KB (服务端使用)
总计: ~95KB (节省 76%)
Streaming SSR 实现:
jsx
// 服务端流式传输,非交互内容先显示
export default function Page() {
return (
<div>
<header>Header</header>
<Suspense fallback={<ArticleSkeleton />}>
<Article /> {/* 可能较慢,但不会阻塞其他内容 */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
使用场景选择:
| 场景 | 推荐方案 |
|---|---|
| 静态内容、无交互 | Server Component |
| 表单、动画、事件 | Client Component |
| SEO 关键页面 | SSR + Server Component |
| 实时数据 | Client Component + SWR |
3.3 Server Component
题目:请详细解释 React Server Components(RSC)的工作原理,以及 Server Component 与 Client Component 的边界如何划分。
参考答案
RSC 核心原理:
xml
┌─────────────────────────────────────────────────────────────┐
│ 服务端 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Server Component 树 │ │
│ │ │ │
│ │ <Page> ← RSC Server 组件 │ │
│ │ <Sidebar> ← RSC (无 JS bundle) │ │
│ │ <InteractiveWidget> ← Client Boundary │ │
│ │ <LikeButton> ← Client (需要水合) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ RSC Payload │
│ (特殊的 JSON 格式描述) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 客户端 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Client 组件树 + 水合 │ │
│ │ │ │
│ │ <Page> (已渲染 HTML) │ │
│ │ <Sidebar> (纯 HTML,无组件) │ │
│ │ <InteractiveWidget> (水合后激活) │ │
│ │ <LikeButton> (客户端运行) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
RSC Payload 格式示例:
javascript
// 服务端返回的 RSC Payload 是特殊格式
// 1. 服务端组件渲染结果
['$', 'div', null, {
className: 'container'
}, ['$', 'h1', null, 'Hello World']]
// 2. 客户端组件引用
['$', 'div', null, {
className: 'container'
}, ['$', 'LikeButton', '$L1', { initialCount: 0 }]]
// ↑ '$L1' 是 client reference,指向 client module
边界划分原则:
jsx
// app/
// ├── page.js ← Server Component (默认)
// ├── layout.js ← Server Component (默认)
// └── components/
// ├── A.js ← Server Component
// ├── B.js ← Server Component
// └── C.client.js ← Client Component (约定后缀)
// 1. 需要 useState/useEffect → Client Component
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// 2. 需要浏览器 API → Client Component
'use client';
export function ScrollToTop() {
if (typeof window !== 'undefined') {
window.scrollTo(0, 0);
}
return <button onClick={() => window.scrollTo(0, 0)}>Top</button>;
}
// 3. 需要第三方库 (可能需要客户端交互) → Client Component
'use client';
import { useEditor } from 'some-editor-lib';
export function Editor() {
const editor = useEditor();
return <div>{editor.content}</div>;
}
// 4. 大型库在服务端用结果 → Server Component
// 服务端处理数据,客户端只负责展示
async function Dashboard() {
const data = await heavyComputation(); // 服务端执行
return <HeavyDataVisualization data={data} />; // 客户端展示
}
详细解析
Server Component 的限制:
jsx
// Server Component 不能:
// 1. 使用 hooks
// 2. 使用浏览器 API
// 3. 使用事件监听
// 4. 使用第三方库的状态管理
// 但可以:
// 1. 直接访问数据库/文件系统
// 2. 使用服务端专属库(如 sharp 处理图片)
// 3. 访问环境变量(安全)
// 4. 大数据处理(不增加客户端负担)
最佳实践 - 数据获取:
jsx
// 在 Server Component 中直接获取数据
// 不需要 useEffect + API 调用
async function BlogList() {
// 直接查询数据库(示例使用 Prisma)
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }
});
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
</li>
))}
</ul>
);
}
组件组合模式:
jsx
// Server Component 作为入口
// 包裹需要交互的部分为 Client Component
async function ProductPage({ id }) {
const product = await getProduct(id);
return (
<div>
<ProductInfo product={product} />
<AddToCart productId={id} initialStock={product.stock} />
<Reviews productId={id} />
</div>
);
}
// AddToCart 需要交互 → Client Component
'use client';
function AddToCart({ productId, initialStock }) {
const [quantity, setQuantity] = useState(1);
return (
<div>
<input
type="number"
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
/>
<button onClick={() => addToCart(productId, quantity)}>
Add to Cart
</button>
</div>
);
}
3.4 streaming
题目:什么是 Streaming SSR?它如何提升用户体验?请在 Next.js App Router 中实现一个支持 streaming 的页面。
参考答案
Streaming 原理:
less
传统 SSR (阻塞):
┌────────────────────────────────────────────────┐
│█████████████████████████████等待数据████████████│
│ ↓ │
│ ┌──────────────────────────────────────────┐ │
│ │ 完整页面 HTML(首字节时间取决于最慢数据) │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
TTFB 慢,用户等待时间长
Streaming SSR (非阻塞):
┌────────────────────────────────────────────────┐
│ ┌────────────┐ ┌────────────────────────┐ │
│ │ 立即响应 │ │ 后续内容逐步到达 │ │
│ │ Header/Nav │ │ Product Details... │ │
│ │ (骨架) │ │ Reviews... │ │
│ └────────────┘ └────────────────────────┘ │
│ TTFB 快 ↑ │
│ 可见内容 │ │
│ 逐步呈现 ↓ │
└────────────────────────────────────────────────┘
实现代码:
jsx
// app/product/[id]/page.js
import { Suspense } from 'react';
import ProductHeader from '@/components/ProductHeader';
import ProductSkeleton from '@/components/ProductSkeleton';
import ReviewsSkeleton from '@/components/ReviewsSkeleton';
// 慢数据组件 - 模拟数据库查询延迟
async function ProductDetails({ id }) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟延迟
const product = await getProduct(id);
return (
<div className="product-details">
<h2>{product.name}</h2>
<p>{product.description}</p>
<p className="price">${product.price}</p>
</div>
);
}
// 更慢的数据组件
async function ProductReviews({ id }) {
await new Promise(resolve => setTimeout(resolve, 3000));
const reviews = await getProductReviews(id);
return (
<div className="reviews">
<h3>Customer Reviews</h3>
{reviews.map(review => (
<div key={review.id} className="review">
<p>{review.content}</p>
<span>⭐ {review.rating}</span>
</div>
))}
</div>
);
}
// 页面组件 - 服务端异步组合
export default async function ProductPage({ params }) {
return (
<div className="product-page">
{/* 1. 立即可用的 Header */}
<ProductHeader productId={params.id} />
{/* 2. 慢数据用 Suspense 包裹 */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails id={params.id} />
</Suspense>
{/* 3. 更慢的 Reviews */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={params.id} />
</Suspense>
</div>
);
}
骨架屏组件:
jsx
// components/ProductSkeleton.jsx
export default function ProductSkeleton() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
<div className="h-4 bg-gray-200 rounded w-5/6 mb-6" />
<div className="h-6 bg-gray-200 rounded w-1/4" />
</div>
);
}
// components/ReviewsSkeleton.jsx
export default function ReviewsSkeleton() {
return (
<div className="animate-pulse">
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4" />
{[1, 2, 3].map(i => (
<div key={i} className="mb-4 p-4 border rounded">
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
<div className="h-4 bg-gray-200 rounded w-2/3" />
</div>
))}
</div>
);
}
详细解析
Streaming 适用场景:
| 场景 | 效果 |
|---|---|
| 产品详情页 | 标题立即显示,详情/评论逐步加载 |
| 搜索结果 | 骨架屏先显示,内容流式加载 |
| 用户 Dashboard | 顶部 KPI 立即显示,图表异步加载 |
| 新闻/文章页 | 文章内容先显示,评论/推荐后加载 |
use hook 实现 loading 状态:
jsx
// next/navigation 提供的 use hook
'use client';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
export function Pagination() {
const searchParams = useSearchParams();
const [page, setPage] = useState(1);
// 配合 URL searchParams 使用
useEffect(() => {
setPage(Number(searchParams.get('page')) || 1);
}, [searchParams]);
return (
<div className="pagination">
<button disabled={page <= 1}>Previous</button>
<span>Page {page}</span>
<button>Next</button>
</div>
);
}
性能指标对比:
yaml
指标对比 (假设最慢数据需 3s):
传统 SSR:
├── TTFB: 3000ms (必须等所有数据)
├── FCP: 3000ms
└── TTI: 3500ms
Streaming SSR:
├── TTFB: 100ms (骨架立即返回)
├── FCP: 100ms
├── LCP: 100ms (骨架先展示)
├── TTI: 3200ms (交互部分最后加载)
└── 用户感知: "页面秒开,内容逐步填充" ✅
四、前端工程化 (T1)
4.1 webpack/vite
题目:Webpack 与 Vite 的核心区别是什么?在 2025-2026 年,如何根据项目场景选择构建工具?
参考答案
核心架构对比:
vbscript
Webpack:
┌─────────────────────────────────────────────────────────┐
│ Build Time │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 入口文件 │ → │ 递归依赖 │ → │ 打包构建 │ │
│ │ index.js │ │ 分析模块 │ │ 输出bundle│ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ 开发时: webpack-dev-server 启动慢 │
│ 冷启动: 扫描所有依赖 → 打包 → 启动服务 (30s+) │
│ 热更新: 重新打包相关模块 → 推送更新 (1-5s) │
└─────────────────────────────────────────────────────────┘
Vite:
┌─────────────────────────────────────────────────────────┐
│ Dev Server │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ HTTP │ │ ESM │ │ 按需 │ │
│ │ Server │ ← │ Native │ ← │ 编译 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ 冷启动: 启动 HTTP 服务 (100ms) │
│ 热更新: 模块级更新,无需打包 (毫秒级) │
└─────────────────────────────────────────────────────────┘
代码示例 - Vite 配置:
javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [
react({
// Fast Refresh 配置
fastRefresh: true,
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
target: 'esnext',
// 分包策略
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'vendor': ['lodash', 'axios'],
},
},
},
},
// 开发服务器配置
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
// 预构建依赖
optimizeDeps: {
include: ['react', 'react-dom', 'lodash'],
},
});
选择指南:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 新项目 / 中小型 | Vite | 极速启动,开发体验好 |
| 大型复杂项目 | Webpack 5 / Vite | 根据团队熟悉度选择 |
| 微前端架构 | Webpack 5 | Module Federation 成熟 |
| 需要兼容旧浏览器 | Webpack 5 | babel-loader 生态完善 |
| React 18 + Server Components | Vite (新版) | ESM native 支持好 |
| 企业内部 legacy 项目 | Webpack 5 | 稳定性优先 |
详细解析
Vite 的依赖预构建:
javascript
// 第一次启动时,Vite 会:
// 1. 扫描 package.json 依赖
// 2. 使用 esbuild 预构建 CJS 依赖为 ESM
// 3. 生成 node_modules/.vite/deps/ 缓存
// 预构建优势:
// - 减少模块请求数量
// - 统一 ESM/CJS 差异
// - 提升热更新速度
生产构建优化:
javascript
// Vite 生产构建使用 Rollup
// 输出优化策略
// 1. CSS 代码分割
import('./dark.css', { rel: 'stylesheet' });
// 2. 动态导入 + 预加载
const { default: HeavyChart } = await import('./HeavyChart.js');
// Rollup 自动生成 <link rel="modulepreload">
// 3. Tree Shaking 优化
// 基于 ESM 的静态分析,未使用的代码自动剔除
import { usedFunction } from 'big-lib';
// → 只打包 usedFunction
热更新速度对比:
markdown
场景:修改一个组件的样式
Webpack:
1. 重新编译该组件模块
2. 重新编译依赖该组件的其他模块
3. 重新生成 chunk
4. 通过 HMR 推送更新
→ 耗时 200ms - 2s
Vite:
1. 直接对改动的文件进行 HMR
2. 通知浏览器更新
→ 耗时 < 50ms
4.2 monorepo
题目:什么是 Monorepo?为什么需要 Monorepo?请用 pnpm + Turborepo 搭建一个前端 Monorepo 项目结构。
参考答案
Monorepo vs Polyrepo:
bash
Polyrepo (多仓库):
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ web │ │ mobile │ │ admin │ │ shared │
│ │ │ │ │ │ │ │
│ @web-ui │ │ @mob-ui │ │ @adm-ui │ │ @ui-lib │
│ npm │ │ npm │ │ npm │ │ npm │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
问题:版本同步难、重复代码、跨仓库修改繁琐
Monorepo (单仓库):
┌─────────────────────────────────────────┐
│ root (pnpm workspace) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ apps │ │ packages│ │ config │ │
│ │ /web │ │ /ui-lib │ │ /eslint │ │
│ │ /admin │ │/utils │ │/tsconf │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 优势:原子提交、共享配置、一站式构建 │
└─────────────────────────────────────────┘
项目结构搭建:
bash
# 目录结构
my-monorepo/
├── pnpm-workspace.yaml # pnpm 工作区配置
├── package.json # 根目录 package
├── turbo.json # Turborepo 配置
├── .npmrc # pnpm 配置
│
├── apps/
│ ├── web/ # React Web 应用
│ │ ├── package.json
│ │ ├── src/
│ │ └── vite.config.ts
│ │
│ └── admin/ # Admin 管理后台
│ ├── package.json
│ └── src/
│
└── packages/
├── ui/ # UI 组件库
│ ├── package.json
│ ├── src/
│ │ ├── Button/
│ │ ├── Input/
│ │ └── index.ts
│ └── tsconfig.json
│
├── utils/ # 工具函数
│ ├── package.json
│ └── src/
│
├── config/ # 共享配置
│ ├── eslint/ # ESLint 配置
│ └── tsconfig/ # TS 基础配置
配置文件:
yaml
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
json
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "build/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
bash
# .npmrc
shamefully-hoist=true
strict-peer-dependencies=false
auto-install-peers=true
json
// packages/ui/package.json
{
"name": "@my-monorepo/ui",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./Button": "./dist/Button/index.js"
},
"scripts": {
"build": "tsup src/index.ts --format esm cjs --dts",
"dev": "tsup src/index.ts --format esm cjs --dts --watch"
}
}
javascript
// apps/web/src/App.tsx
import { Button, Input } from '@my-monorepo/ui';
import { formatDate } from '@my-monorepo/utils';
function App() {
return (
<div>
<Button>Click me</Button>
<Input placeholder="Enter date" />
<p>{formatDate(new Date())}</p>
</div>
);
}
详细解析
pnpm vs yarn/npm 优势:
bash
# 磁盘空间优化
pnpm: 硬链接 + 符号链接 → 共享同一依赖文件
# 项目A和项目B都用react → 只存储一份react文件
# 依赖隔离
pnpm: 每个项目有自己的 node_modules/.pnpm
# 避免幽灵依赖问题
Turborepo 任务调度:
bash
# 安装
pnpm add turbo -D -w
# 运行构建(自动分析依赖关系,按序执行)
pnpm turbo build
# Turborepo 自动:
# 1. 分析 packages 间的依赖关系
# 2. 构建依赖树
# 3. 并行执行无依赖任务
# 4. 缓存构建结果
Monorepo 适用场景:
| 场景 | 是否 Monorepo |
|---|---|
| 多应用共享组件/工具 | ✅ 强烈推荐 |
| 微前端架构 | ✅ 适合 |
| 全栈应用 (FE + BE) | ✅ 可选 |
| 单个小应用 | ❌ 过度工程 |
| 完全独立的多个产品 | ❌ 维护成本高 |
4.3 微前端
题目:什么是微前端?它解决了什么问题?请实现一个基于 iframe 的简单微前端架构,并说明其他微前端方案的适用场景。
参考答案
微前端解决的问题:
makefile
巨石应用问题:
┌─────────────────────────────────────────────────┐
│ monolith-app │
│ ┌─────────────────────────────────────────┐ │
│ │ 10万+ 行代码 │ │
│ │ 技术栈混乱 (jQuery + Vue 2 + React 18) │ │
│ │ 团队耦合 → 发布需要协调所有人 │ │
│ │ 单点故障 → 一个bug影响整个系统 │ │
│ │ 本地开发启动慢 (5分钟+) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
微前端解决:
┌─────────────────────────────────────────────────┐
│ 主应用容器 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 营销模块 │ │ 订单模块 │ │ 用户模块 │ │
│ │ (Vue 2) │ │ (React) │ │ (Vue 3) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ✅ 独立开发 ✅ 独立部署 ✅ 技术异构 │
└─────────────────────────────────────────────────┘
基于 iframe 的微前端实现:
html
<!-- 主应用: index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>微前端主应用</title>
<style>
.micro-container {
border: 1px solid #ddd;
padding: 20px;
margin: 10px 0;
}
.nav-link {
margin: 10px;
padding: 5px 10px;
cursor: pointer;
background: #f0f0f0;
border-radius: 4px;
}
.active {
background: #007bff;
color: white;
}
</style>
</head>
<body>
<h1>微前端主应用</h1>
<nav>
<span class="nav-link active" data-app="vue-app">Vue 子应用</span>
<span class="nav-link" data-app="react-app">React 子应用</span>
</nav>
<div id="micro-content">
<!-- 子应用挂载点 -->
</div>
<script>
// 微前端管理器
class MicroFrontend {
constructor() {
this.apps = {
'vue-app': {
url: 'http://localhost:3001/index.html',
container: document.getElementById('vue-container')
},
'react-app': {
url: 'http://localhost:3002/index.html',
container: document.getElementById('react-container')
}
};
this.currentApp = null;
}
// 挂载子应用
mount(appName) {
if (this.currentApp === appName) return;
const content = document.getElementById('micro-content');
const config = this.apps[appName];
// 清除当前内容
content.innerHTML = '';
// 创建 iframe
const iframe = document.createElement('iframe');
iframe.src = config.url;
iframe.style.width = '100%';
iframe.style.height = '500px';
iframe.style.border = 'none';
content.appendChild(iframe);
this.currentApp = appName;
// 更新导航高亮
document.querySelectorAll('.nav-link').forEach(el => {
el.classList.toggle('active', el.dataset.app === appName);
});
}
// 跨应用通信
postMessage(appName, action, data) {
const config = this.apps[appName];
if (config && config.iframe) {
config.iframe.contentWindow.postMessage({ action, data }, '*');
}
}
}
const micro = new MicroFrontend();
// 导航切换
document.querySelectorAll('.nav-link').forEach(el => {
el.addEventListener('click', () => {
micro.mount(el.dataset.app);
});
});
// 默认加载第一个应用
micro.mount('vue-app');
</script>
</body>
</html>
通信机制:
javascript
// 子应用通信脚本: communication.js
// 监听来自主应用的消息
window.addEventListener('message', (event) => {
const { action, data } = event.data;
switch (action) {
case 'user-login':
handleUserLogin(data);
break;
case 'navigate':
router.push(data.path);
break;
case 'get-user-info':
// 回复主应用
event.source.postMessage({
type: 'user-info-response',
data: getCurrentUser()
}, '*');
break;
}
});
// 向主应用发送消息
function notifyMain(action, data) {
window.parent.postMessage({ action, data }, '*');
}
详细解析
微前端方案对比:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| iframe | 独立浏览器上下文 | 隔离性强、实现简单 | 性能差、样式冲突 | 简单场景、 legacy 迁移 |
| qiankun | JS 沙箱 + HTML Entry | 功能完整、社区活跃 | 包体积大 | 中大型项目 |
| single-spa | 生命周期管理 | 灵活、轻量 | 配置复杂 | 定制化需求 |
| Module Federation | Webpack 5 远程模块 | 原生、性能好 | 配置复杂 | 新项目 |
| EMP | 微模块化 | 滴滴开源 | 生态较小 | 特定场景 |
Module Federation 示例(推荐新项目):
javascript
// host/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// 引用远程子应用
vueApp: 'vueApp@http://localhost:3001/remoteEntry.js',
reactApp: 'reactApp@http://localhost:3002/remoteEntry.js',
},
shared: ['react', 'react-dom', 'vue'],
}),
],
};
// 子应用 webpack.config.js (Vue App)
new ModuleFederationPlugin({
name: 'vueApp',
filename: 'remoteEntry.js', // 暴露给宿主
exposes: {
'./App': './src/App.vue', // 暴露的模块
},
shared: ['vue'],
});
4.4 BFF
题目:什么是 BFF(Backend For Frontend)?它与网关层有什么区别?请说明 BFF 在前端架构中的价值,并给出 NestJS 实现 BFF 的示例。
参考答案
BFF 架构定位:
bash
┌─────────────────────────────────────────────────────────────┐
│ 客户端 │
│ (Web / Mobile / App) │
└──────────────────────────┬──────────────────────────────────┘
│ HTTP/gRPC
┌──────────────────────────▼──────────────────────────────────┐
│ BFF 层 │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Web BFF │ │ Mobile BFF │ │ App BFF │ │
│ │ /bff/web/* │ │ /bff/mobile/* │ │ /bff/app/* │ │
│ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ │
└──────────┼──────────────────┼──────────────────┼──────────┘
│ 聚合/裁剪 │ 聚合/裁剪 │ 聚合/裁剪
┌──────────▼──────────────────▼──────────────────▼──────────┐
│ Gateway / API Gateway │
│ (统一入口、鉴权、限流、路由) │
└──────────────────────────┬──────────────────────────────────┘
│ 微服务调用
┌──────────────────────────▼──────────────────────────────────┐
│ 微服务层 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ User │ │ Product│ │ Order │ │ Payment│ │
│ │ Service│ │ Service│ │ Service│ │ Service│ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
└─────────────────────────────────────────────────────────────┘
BFF vs API Gateway:
| 维度 | API Gateway | BFF |
|---|---|---|
| 层级 | 网关层 | 聚合层 |
| 职责 | 统一鉴权、限流、路由 | 业务聚合、数据裁剪 |
| 粒度 | 请求级别 | 接口级别 |
| 定制 | 通用配置 | 按端定制 |
| 位置 | 更靠近入口 | 靠近后端服务 |
NestJS BFF 实现:
typescript
// bff-web/src/modules/product-bff/product-bff.controller.ts
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ProductService } from '@/services/product.service';
import { ReviewService } from '@/services/review.service';
import { InventoryService } from '@/services/inventory.service';
@ApiTags('BFF - Web 产品接口')
@Controller('bff/web/products')
export class ProductBffController {
constructor(
private readonly productService: ProductService,
private readonly reviewService: ReviewService,
private readonly inventoryService: InventoryService,
) {}
/**
* Web 端产品详情聚合接口
* 一次性返回:产品信息 + 评分统计 + 库存状态 + 推荐商品
*/
@Get(':id')
@ApiOperation({ summary: 'Web 产品详情聚合' })
async getProductDetail(@Param('id') id: string) {
// 并行请求多个服务
const [product, reviews, inventory, recommendations] = await Promise.all([
this.productService.findById(id),
this.reviewService.getStats(id),
this.inventoryService.check(id),
this.productService.getRecommendations(id, 6),
]);
// 按 Web 端需求聚合数据
return {
// 产品基础信息
info: {
id: product.id,
name: product.name,
price: product.price,
images: product.images.slice(0, 5), // 只返回5张图
description: product.description,
},
// 评分聚合
rating: {
average: reviews.average,
total: reviews.total,
distribution: reviews.distribution,
},
// 库存状态(Web 端需要详细信息)
stock: {
available: inventory.available,
quantity: inventory.quantity,
status: inventory.status, // in_stock / low_stock / out_of_stock
estimatedRestock: inventory.estimatedRestock,
},
// 简短推荐(移动端可能不需要)
recommendations: recommendations.map(p => ({
id: p.id,
name: p.name,
price: p.price,
thumbnail: p.thumbnail,
})),
};
}
}
typescript
// bff-web/src/services/product.service.ts
import { Injectable, HttpException } from '@nestjs/common';
import { ProductGrpcClient } from '@/grpc/product.client';
@Injectable()
export class ProductService {
constructor(private readonly grpcClient: ProductGrpcClient) {}
async findById(id: string) {
try {
return await this.grpcClient.product.getProduct({ id });
} catch (error) {
throw new HttpException('产品不存在', 404);
}
}
async getRecommendations(id: string, limit: number) {
// 调用推荐服务
const response = await this.grpcClient.recommend.getRecommendations({
productId: id,
limit,
});
return response.products;
}
}
详细解析
BFF 的核心价值:
javascript
// 1. 数据聚合 - 减少客户端请求
// 无 BFF:客户端需要发 5 个请求
const [product, reviews, inventory, cart, user] = await Promise.all([
fetch('/api/products/1'),
fetch('/api/products/1/reviews/stats'),
fetch('/api/inventory/1'),
fetch('/api/cart'),
fetch('/api/user/profile'),
]);
// 有 BFF:只需 1 个请求
const data = await fetch('/bff/web/products/1');
// 2. 数据裁剪 - 减少传输量
// Web 端需要完整信息
// Mobile 端可能只需要精简数据
// BFF 可以按需返回
// 3. 协议转换
// 内部 gRPC,外部 REST
// BFF 做协议转换
// 4. 业务逻辑适配
// Web 端和 Mobile 端的业务逻辑可能不同
// BFF 分别处理
SSR BFF 场景:
typescript
// Next.js API Routes 作为 BFF
// app/api/bff/product/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request, { params }) {
const { id } = params;
// 服务端 BFF - 直接访问后端服务
const [product, reviews] = await Promise.all([
fetch(`${INTERNAL_API}/products/${id}`).then(r => r.json()),
fetch(`${INTERNAL_API}/reviews/product/${id}/stats`).then(r => r.json()),
]);
// 返回适合 SSR 的数据
return NextResponse.json({
...product,
reviewStats: reviews,
// SEO 相关元数据
meta: {
title: product.seoTitle || product.name,
description: product.seoDescription || product.description.slice(0, 160),
},
});
}
五、前端性能优化 (T1)
5.1 首屏优化
题目:如何优化首屏加载性能?请从加载、解析、执行三个阶段详细说明,并给出具体可落地的优化方案。
参考答案
首屏渲染时间线:
swift
┌─────────────────────────────────────────────────────────────────┐
│ 首屏优化三阶段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 加载阶段 │
│ ├─ DNS 解析 → TCP 连接 → TLS 握手 → HTTP 请求 │
│ ├─ 优化: DNS 预解析 / 减少请求数 / CDN 加速 │
│ │
│ 2️⃣ 解析阶段 │
│ ├─ HTML 解析 → 构建 DOM → CSS 解析 → 构建 CSSOM │
│ ├─ 优化: defer/async 脚本 / CSS 精简 │
│ │
│ 3️⃣ 执行阶段 │
│ ├─ JS 执行 → 渲染树合并 → Layout → Paint → Composite │
│ ├─ 优化: 代码分割 / 懒加载 / Tree Shaking │
│ │
└─────────────────────────────────────────────────────────────────┘
具体优化方案:
html
<!-- 1. DNS 预解析 + 预连接 -->
<link rel="dns-prefetch" href="//static.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<!-- 2. 关键 CSS 内联,非关键 CSS 异步加载 -->
<style>
/* 关键渲染路径 CSS */
.critical-header { background: #333; color: #fff; }
</style>
<link rel="preload" href="/styles/non-critical.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/non-critical.css"></noscript>
<!-- 3. 脚本加载策略 -->
<!-- defer: 解析完 HTML 后执行,不阻塞渲染 -->
<script src="/app.js" defer></script>
<!-- async: 下载完立即执行,不保证顺序 -->
<script src="/analytics.js" async></script>
<!-- 模块脚本 (推荐) -->
<script type="module" src="/app.mjs"></script>
javascript
// 4. 资源预加载
// 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/images/hero.webp" as="image">
// 预加载路由组件
const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{
path: 'dashboard',
// 预加载 dashboard 组件
lazy: () => import('./pages/Dashboard'),
},
],
},
]);
javascript
// 5. 路由级代码分割 (React + Vite)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
css
/* 6. CSS 优化 */
.critical-path {
/* 避免通配符 */
/* 避免深层嵌套 */
/* 使用 CSS 变量复用 */
--primary-color: #007bff;
color: var(--primary-color);
}
javascript
// 7. 图片优化
// WebP 格式 + 响应式图片
<img
src="/images/hero.webp"
srcset="
/images/hero-320.webp 320w,
/images/hero-640.webp 640w,
/images/hero-1280.webp 1280w
"
sizes="(max-width: 640px) 100vw, 50vw"
loading="lazy" /* 懒加载 */
decoding="async"
alt="Hero image"
/>
详细解析
FCP / LCP 优化:
javascript
// 监控关键指标
import { onFCP, onLCP, onCLS } from 'web-vitals';
onFCP((metric) => {
console.log('FCP:', metric.value);
// FCP > 1.8s → 需要优化
});
onLCP((metric) => {
console.log('LCP:', metric.value, metric.entries);
// LCP 通常是最大图片或标题
// 检查: LCP 元素是否在首屏、是否延迟加载
});
// 常见 LCP 优化
// 1. 预加载 LCP 图片
<link rel="preload" as="image" href="/hero.webp">
// 2. 确保 LCP 图片使用现代格式
<img src="/hero.avif" loading="eager" fetchpriority="high">
服务端优化:
javascript
// SSR + 流式传输
// Next.js App Router
export default async function Page() {
return (
<div>
<Header /> {/* 立即渲染 */}
<Suspense fallback={<Loading />}>
<HeavyContent /> {/* 流式加载 */}
</Suspense>
</div>
);
}
// HTTP 缓存策略
// next.config.js
module.exports = {
headers: [
{
source: '/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
],
};
5.2 长列表优化
题目:如何优化渲染大量数据的列表(长列表)?请对比不同的优化方案,并实现一个高性能的虚拟滚动组件。
参考答案
长列表优化方案对比:
| 方案 | 原理 | 适用场景 | 性能 |
|---|---|---|---|
| 分页 | 限制每页数量 | 数据量固定 | ⭐⭐⭐⭐⭐ |
| 懒加载 | 滚动到底部加载 | 无限滚动 | ⭐⭐⭐⭐ |
| 虚拟滚动 | 只渲染可见区域 | 超大数据集 | ⭐⭐⭐⭐⭐ |
| 时间分片 | 每帧渲染部分 | 中等数据集 | ⭐⭐⭐ |
| 懒渲染 | 只渲染可视区域 | 图片为主的列表 | ⭐⭐⭐ |
虚拟滚动实现:
tsx
// VirtualList.tsx - 基于 React 的虚拟滚动组件
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
interface VirtualListProps<T> {
items: T[];
height: number; // 列表容器高度
itemHeight: number; // 每项固定高度
overscan?: number; // 预渲染区域大小
renderItem: (item: T, index: number) => React.ReactNode;
}
export function VirtualList<T>({
items,
height,
itemHeight,
overscan = 3,
renderItem,
}: VirtualListProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
// 计算可见范围
const visibleRange = useMemo(() => {
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + height) / itemHeight) + overscan
);
return { startIndex, endIndex };
}, [scrollTop, height, itemHeight, items.length, overscan]);
// 可见项
const visibleItems = useMemo(() => {
const { startIndex, endIndex } = visibleRange;
return items.slice(startIndex, endIndex + 1).map((item, i) => ({
item,
index: startIndex + i,
}));
}, [items, visibleRange]);
// 滚动处理
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
// 总高度占位
const totalHeight = items.length * itemHeight;
// 偏移量(使可见区域对齐)
const offsetY = visibleRange.startIndex * itemHeight;
return (
<div
ref={containerRef}
style={{
height: `${height}px`,
overflow: 'auto',
position: 'relative',
}}
onScroll={handleScroll}
>
{/* 内容占位器 */}
<div style={{ height: `${totalHeight}px`, position: 'relative' }}>
{/* 可见内容 */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
transform: `translateY(${offsetY}px)`,
}}
>
{visibleItems.map(({ item, index }) => (
<div key={index} style={{ height: `${itemHeight}px` }}>
{renderItem(item, index)}
</div>
))}
</div>
</div>
</div>
);
}
// 使用示例
function UserList() {
// 模拟 100000 条数据
const users = useMemo(() =>
Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
})), []);
return (
<VirtualList
items={users}
height={600}
itemHeight={50}
overscan={5}
renderItem={(user) => (
<div style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
<strong>{user.name}</strong>
<span style={{ marginLeft: '10px', color: '#666' }}>{user.email}</span>
</div>
)}
/>
);
}
进阶:动态高度虚拟滚动:
tsx
// 使用 ResizeObserver 测量动态高度
export function DynamicVirtualList({ items, renderItem }) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const [itemHeights, setItemHeights] = useState<number[]>([]);
const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map());
// 计算每个元素的位置偏移
const itemOffsets = useMemo(() => {
const offsets = [0];
itemHeights.forEach((h, i) => {
offsets.push(offsets[i] + (h || 60)); // 默认 60px
});
return offsets;
}, [itemHeights]);
// 二分查找找到起始索引
const startIndex = useMemo(() => {
let low = 0, high = itemOffsets.length - 1;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (itemOffsets[mid] < scrollTop) low = mid + 1;
else high = mid;
}
return Math.max(0, low - 1);
}, [scrollTop, itemOffsets]);
// 测量单个元素高度
const measureItem = useCallback((index: number, element: HTMLDivElement | null) => {
if (element) {
itemRefs.current.set(index, element);
setItemHeights(prev => {
const newHeights = [...prev];
newHeights[index] = element.offsetHeight;
return newHeights;
});
}
}, []);
return (
<div
ref={containerRef}
style={{ height: '600px', overflow: 'auto' }}
onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: itemOffsets[itemOffsets.length - 1] }}>
{items.slice(startIndex, startIndex + 10).map((item, i) => (
<div
key={startIndex + i}
ref={el => measureItem(startIndex + i, el)}
>
{renderItem(item, startIndex + i)}
</div>
))}
</div>
</div>
);
}
详细解析
React 官方虚拟列表库:
tsx
// @tanstack/react-virtual (推荐)
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // 估算高度
overscan: 5, // 预渲染数量
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: virtualItem.start,
width: '100%',
}}
>
{items[virtualItem.index].content}
</div>
))}
</div>
</div>
);
}
性能指标对比:
makefile
渲染 10000 条数据:
普通列表:
├── DOM 节点数: 10000+
├── 初始渲染: ~2000ms
├── 滚动帧率: <30fps
└── 内存占用: 高
虚拟滚动:
├── DOM 节点数: ~20-30 (可见区域 + overscan)
├── 初始渲染: ~50ms
├── 滚动帧率: 60fps
└── 内存占用: 低
5.3 虚拟滚动
题目:请详细解释虚拟滚动的实现原理,并实现一个支持动态加载更多数据的无限滚动虚拟列表。
参考答案
虚拟滚动核心原理:
scss
┌─────────────────────────────────────────────────────────────┐
│ 虚拟滚动原理 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 滚动容器 (600px) │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 可见区域 (只渲染可见项) │ │ │
│ │ │ ┌─────────────────────────────────────────┐ │ │ │
│ │ │ │ Item 5 │ │ │ │
│ │ │ │ Item 6 │ │ │ │
│ │ │ │ Item 7 ← 渲染 │ │ │ │
│ │ │ │ Item 8 │ │ │ │
│ │ │ │ Item 9 │ │ │ │
│ │ │ └─────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ⬆ Item 0-4 (不可见,不渲染) │ │
│ │ ⬇ Item 10-9999 (不可见,不渲染) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键: 通过 transform: translateY() 定位可见区域 │
└─────────────────────────────────────────────────────────────┘
无限滚动虚拟列表实现:
tsx
// InfiniteVirtualList.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
interface InfiniteVirtualListProps<T> {
fetchMore: (page: number) => Promise<T[]>; // 加载更多数据的函数
hasMore: boolean; // 是否还有更多数据
itemHeight: number; // 固定行高
containerHeight: number; // 容器高度
renderItem: (item: T, index: number) => React.ReactNode;
loadingComponent?: React.ReactNode; // 加载中组件
}
export function InfiniteVirtualList<T extends { id: string | number }>({
fetchMore,
hasMore,
itemHeight,
containerHeight,
renderItem,
loadingComponent,
}: InfiniteVirtualListProps<T>) {
const [items, setItems] = useState<T[]>([]);
const [scrollTop, setScrollTop] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(1);
const containerRef = useRef<HTMLDivElement>(null);
const loadingRef = useRef(false);
// 加载初始数据和更多数据
const loadMore = useCallback(async () => {
if (loadingRef.current || !hasMore) return;
loadingRef.current = true;
setIsLoading(true);
try {
const newItems = await fetchMore(page);
setItems(prev => [...prev, ...newItems]);
setPage(p => p + 1);
} finally {
loadingRef.current = false;
setIsLoading(false);
}
}, [fetchMore, page, hasMore]);
// 初始加载
useEffect(() => {
loadMore();
}, []);
// 计算可见范围
const visibleRange = {
startIndex: Math.max(0, Math.floor(scrollTop / itemHeight) - 5),
endIndex: Math.ceil((scrollTop + containerHeight) / itemHeight) + 5,
};
// 只渲染可见项
const visibleItems = items
.slice(visibleRange.startIndex, visibleRange.endIndex)
.map((item, i) => ({
item,
index: visibleRange.startIndex + i,
}));
// 处理滚动
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
setScrollTop(scrollTop);
// 检测是否滚动到底部
if (scrollHeight - scrollTop - clientHeight < 200) {
loadMore();
}
}, [loadMore]);
// 总内容高度
const totalHeight = items.length * itemHeight;
const offsetY = visibleRange.startIndex * itemHeight;
return (
<div
ref={containerRef}
style={{ height: `${containerHeight}px`, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: `${totalHeight}px`, position: 'relative' }}>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
transform: `translateY(${offsetY}px)`,
}}
>
{visibleItems.map(({ item, index }) => (
<div
key={item.id}
style={{ height: `${itemHeight}px` }}
>
{renderItem(item, index)}
</div>
))}
{/* 加载中状态 */}
{isLoading && (
<div style={{ height: `${itemHeight}px`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{loadingComponent || <span>加载中...</span>}
</div>
)}
{/* 没有更多数据 */}
{!hasMore && items.length > 0 && (
<div style={{ height: `${itemHeight}px`, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}>
已加载全部
</div>
)}
</div>
</div>
</div>
);
}
使用示例:
tsx
// Feed 流组件
function FeedList() {
const [hasMore, setHasMore] = useState(true);
const fetchMoreFeeds = async (page: number) => {
const response = await fetch(`/api/feeds?page=${page}&limit=20`);
const data = await response.json();
if (data.feeds.length === 0) {
setHasMore(false);
return [];
}
return data.feeds;
};
return (
<InfiniteVirtualList
fetchMore={fetchMoreFeeds}
hasMore={hasMore}
itemHeight={120}
containerHeight={window.innerHeight - 64}
renderItem={(feed, index) => (
<FeedCard feed={feed} />
)}
loadingComponent={<Spin />}
/>
);
}
详细解析
性能优化技巧:
tsx
// 1. 使用 React.memo 避免不必要的重渲染
const VirtualItem = React.memo(({ item, index, style }) => (
<div style={style}>
<ItemContent data={item} />
</div>
), (prevProps, nextProps) => {
return prevProps.item.id === nextProps.item.id;
});
// 2. 使用 CSS contain 隔离重排
<div style={{ contain: 'strict' }}>
{/* 列表项内容 */}
</div>;
// 3. 使用 will-change 提示浏览器
<div style={{ willChange: 'transform' }}>
{/* 内容 */}
</div>;
注意事项:
markdown
1. 固定高度 vs 动态高度
- 固定高度:性能最优,O(1) 定位
- 动态高度:需要维护偏移量数组,O(log n) 定位
2. 滚动位置重置
- 加载新数据时,如需保持滚动位置
- 记录 scrollTop,计算新的偏移量
3. 内存泄漏
- 清理 useEffect 中的事件监听
- 使用 useRef 保存最新的 items
4. 键盘可访问性
- 虚拟列表默认不可被屏幕阅读器遍历
- 需配合 ARIA 实现可访问性
5.4 tree shaking
题目:什么是 Tree Shaking?它是如何工作的?请说明如何正确编写代码以确保 Tree Shaking 生效,并解释 ES Module 与 CommonJS 的区别。
参考答案
Tree Shaking 原理:
javascript
┌─────────────────────────────────────────────────────────────┐
│ Tree Shaking │
│ │
│ 输入: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ import { a, b } from 'utils'; │ │
│ │ │ │
│ │ export function a() { return 'a'; } ← 使用 │ │
│ │ export function b() { return 'b'; } ← 使用 │ │
│ │ export function c() { return 'c'; } ← 未使用 → 删除 │ │
│ │ export function d() { return 'd'; } ← 未使用 → 删除 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 静态分析 │
│ ↓ │
│ 输出: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ function a() { return 'a'; } ← 保留 │ │
│ │ function b() { return 'b'; } ← 保留 │ │
│ │ // c 和 d 被移除 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
ES Module vs CommonJS:
| 特性 | ES Module | CommonJS |
|---|---|---|
| 语法 | import/export |
require/module.exports |
| 加载 | 静态,编译时确定 | 动态,运行时确定 |
| 绑定 | 值引用 | 值拷贝 |
| Tree Shaking | ✅ 支持 | ❌ 不支持 |
| 循环引用 | 绑定延迟 | 可能出问题 |
javascript
// ES Module - 静态结构,允许 Tree Shaking
import { flatten } from 'lodash'; // 可分析,未使用的排除
export const add = (a, b) => a + b;
// CommonJS - 动态结构,无法 Tree Shaking
const _ = require('lodash'); // 整个模块导入
module.exports = { add }; // 整个导出
Tree Shaking 正确姿势:
javascript
// ❌ 错误:使用默认导出,整个模块被打包
import lodash from 'lodash';
lodash.flatten([1, [2, 3]]);
// ✅ 正确:使用命名导出
import { flatten } from 'lodash-es'; // lodash-es 支持 ESM
flatten([1, [2, 3]]);
// ✅ 正确:路径导入
import flatten from 'lodash/flatten'; // 需要 lodash 版本 >= 4
// ✅ 正确:使用 @非常好用
import { flatten } from 'es-toolkit'; // 专为 Tree Shaking 设计
Vite/Webpack 配置:
javascript
// vite.config.js
export default defineConfig({
build: {
target: 'esnext',
minify: 'esbuild', // esbuild 原生支持 Tree Shaking
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// 分包,触发各自模块的 Tree Shaking
return 'vendor';
}
},
},
},
},
// esbuild 配置
esbuild: {
treeShaking: true,
},
});
javascript
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动开启 Tree Shaking
optimization: {
usedExports: true, // 标记未使用的导出
sideEffects: true, // 启用 Tree Shaking
providedExports: true, // 收集导出信息
},
};
sideEffects 配置:
json
// package.json
{
"sideEffects": [
"*.css",
"*.scss",
"./src/styles/global.css",
"!./src/utils/analytics.js" // 排除,假设它没有副作用
]
}
javascript
// 如果模块声明无副作用,可安全删除未使用的导出
// 假设 utils.js 声明了 sideEffects: false
export function used() { /* ... */ }
export function unused() { /* ... */ }
// 如果 unused() 没被使用,整个函数会被移除
详细解析
副作用与纯函数:
javascript
// 有副作用 - 不能 Tree Shaking
function incrementGlobal() {
window.counter = (window.counter || 0) + 1; // 修改全局状态
}
import { incrementGlobal } from './utils';
incrementGlobal(); // 必须保留,副作用存在
// 无副作用 - 可以 Tree Shaking
function add(a, b) {
return a + b; // 纯函数
}
export { add };
// 如果未使用,add 可以被移除
常见问题排查:
javascript
// 问题 1:转译器将 ESM 转成 CJS
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
modules: false, // 保持 ES Module,供构建工具 Tree Shaking
}],
],
};
// 问题 2:使用了动态导入
import('./module').then(module => {
module.doSomething(); // 动态,无法 Tree Shaking
});
// 问题 3:第三方库未配置 sideEffects
// 在库的 package.json 添加
{
"sideEffects": false
}
面试题生成完毕
文件路径:
/Users/bytedance/my-life/my-interview/generated_interview_fronted_20260510.md共覆盖 5 个子类别 、20 个 topics,包含代码实现、详细解析和 2025-2026 年最新技术趋势。