防抖:竟态条件处理(搜索框取消请求方法):
1.标志位法:
javascript
let requestId = 0;
useEffect(() => {
const currentId = ++requestId;
fetch(url).then((data) => {
// 重点:这个 function 带走了 currentId
console.log(currentId);
if(currentID===requestID){
setResults(data)
}
});
}, [value]);
为什么请求发出去很久之后,
currentId还能记住当时的数字? 比如第 1 次请求currentId=1,过了 3 秒请求才回来,为什么它还知道自己是 1,而不是变成最新的 3?
答案就是:**闭包 = 函数带走了自己的 "专属小背包"**我用最简单的方式讲:
闭包核心:函数在定义时,会把它周围的变量 "打包带走",永远跟着它,永远不变。 不管过多久,不管外面变量怎么变,函数带走的那个值,永远是当时定义时的值。
关键:每次 useEffect 运行,都会创造一个全新的独立环境!
模拟 3 次快速触发(最直观):
你连续快速触发 3 次,等于创建了 3 个独立的函数 + 3 个独立的 currentId。
2. 什么都不做:
javascript
// 让所有请求都完成,最后一个覆盖前面的
fetch(url).then(data => setResults(data));
缺点: 竞态条件 - 如果慢请求后返回,会覆盖快请求的正确结果。
3.AbortController:
javascript
const controllerRef=useRef(null)
useEffect(() => {
if (!value) return; // 空值直接返回
// 1️⃣ 设置定时器(防抖)
const timer = setTimeout(() => {
// 2️⃣ 取消上一次请求
if (controllerRef.current) {
controllerRef.current.abort(); // 中断旧请求
}
// 3️⃣ 创建新的控制器
const controller = new AbortController();
controllerRef.current = controller;
// 4️⃣ 发起新请求
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setResults(data))
.catch(err => {
// 5️⃣ 过滤 AbortError(正常取消不算错误)
if (err.name !== 'AbortError') {
console.log(err);
}
});
}, 300);
// 6️⃣ 清理函数:组件卸载或 value 变化时执行
return () => {
clearTimeout(timer);
if (controllerRef.current) {
controllerRef.current.abort();
}
};
}, [value]);
1. Virtual DOM:为什么需要它?
核心问题:直接操作 DOM 太慢太贵。 真实 DOM 是浏览器内核构造的巨型对象。每操作一次 DOM,浏览器都要:重新计算样式(Recalculate Style),重新布局(Layout / Reflow),重新绘制(Paint),重新合成(Composite)。
频繁操作 = 频繁重排重绘 = 页面卡顿、掉帧。
VDOM: Virtual DOM = 用轻量 JS 对象,模拟真实 DOM 树。
javascript
// ❌ 传统方式:每次都操作真实 DOM
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
document.body.appendChild(div); // 触发 1000 次重排/重绘
}
// ✅ React 方式:批量更新
const items = Array.from({ length: 1000 }, (_, i) => <div key={i}>{i}</div>);
// React 会计算最小变更,一次性更新 DOM
Virtual DOM 工作流程:
javascript
1. 状态变化 → 2. 生成新 VDOM树 → 3. Diff 算法对比 → 4. 最小化 DOM 操作
实际例子:
javascript
// 旧 VNode
const oldVNode = {
type: 'ul',
children: [
{ type: 'li', children: 'A' },
{ type: 'li', children: 'B' },
]
};
// 新 VNode
const newVNode = {
type: 'ul',
children: [
{ type: 'li', children: 'A' }, // 不变
{ type: 'li', children: 'C' }, // 改变
{ type: 'li', children: 'D' }, // 新增
]
};
// Diff 结果:只更新第 2 个 li 的文本,新增第 3 个 li
Virtual DOM 整体流程:
初次渲染 组件生成 VDOM 树 → 渲染成 真实 DOM
状态更新 组件重新生成 新 VDOM 树
Diff 算法(三大策略)
- 同层比较:上层变,整棵子树重建
- 类型比较:类型不同直接重建;类型相同只更属性
- 列表比较 :用 key 识别节点,最大化复用
最终更新 React 根据 Diff 结果,只操作变化的真实 DOM,实现最小更新。
key 必须唯一且稳定,绝对不能用 index。
2. Diff 算法:三大策略
策略 1:同层比较(Tree Diff):如果上层 DOM 元素更改,那么上层和下层的 DOM 树会整体新建。
- 父节点类型变了(div → span)
- React 直接销毁整棵子树
- 重新创建全新节点
- 绝不跨层级移动、复用
javascript
// ❌ 不会跨层级比较
<div> <span>
<span>A</span> <span>A</span>
</div> → </span>
// React 会:删除 div 和 span,创建新的 span 和 span
// 不会:移动 span 到 span 下
策略 2:类型比较(Component Diff):如果是类型更改,直接替换这部分新的 VDOM 为 DOM。如果类型相同,只改 ClassName,原生 DOM 只更新属性。
javascript
// 类型不同 → 直接替换
<div>Hello</div> → <span>Hello</span>
// React:删除 div,创建 span
// 类型相同 → 更新属性
<div className="old">Hello</div> → <div className="new">Hello</div>
// React:只更新 className
策略 3:列表比较(Element Diff)- key 的作用
没有 key 的问题:
javascript
// 旧列表
<ul>
<li>A</li>
<li>B</li>
</ul>
// 新列表(在头部插入 C)
<ul>
<li>C</li>
<li>A</li>
<li>B</li>
</ul>
// ❌ 没有 key:React 会认为
// 第 1 个 li: A → C (更新)
// 第 2 个 li: B → A (更新)
// 第 3 个 li: 新增 B (创建)
// 结果:3 次操作
有 key 的优化:
javascript
// 旧列表
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// 新列表
<ul>
<li key="c">C</li>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// ✅ 有 key:React 识别出
// key="a" 和 key="b" 没变,只是移动了位置
// key="c" 是新增的
// 结果:1 次插入操作
实战案例:为什么不能用 index 做 key?
javascript
function TodoList() {
const [todos, setTodos] = useState(['A', 'B', 'C']);
// ❌ 错误:用 index 做 key
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input type="checkbox" />
{todo}
</li>
))}
</ul>
);
}
// 问题演示:
// 初始状态:
// [0] A ☐
// [1] B ☐
// [2] C ☐
// 用户勾选 B,然后删除 A:
// [0] B ☑ (原来的 [1])
// [1] C ☐ (原来的 [2])
// React 看到的:
// key=0: A → B (更新文本)
// key=1: B → C (更新文本)
// key=2: 删除
// 结果:checkbox 的选中状态错位了!B 的勾选丢失
正确做法:
javascript
// ✅ 用唯一 ID 做 key
{todos.map(todo => (
<li key={todo.id}>
<input type="checkbox" />
{todo.text}
</li>
))}
3. 生命周期:类组件 vs Hooks
类组件生命周期:
javascript
class MyComponent extends React.Component {
// 1. 挂载阶段
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
// DOM 已渲染,可以:
// - 发起请求
// - 订阅事件
// - 操作 DOM
console.log('组件挂载完成');
}
// 2. 更新阶段
shouldComponentUpdate(nextProps, nextState) {
// 返回 false 可以阻止渲染(性能优化)
return nextState.count !== this.state.count;
}
componentDidUpdate(prevProps, prevState) {
// 更新后执行
if (prevState.count !== this.state.count) {
console.log('count 变化了');
}
}
// 3. 卸载阶段
componentWillUnmount() {
// 清理:取消订阅、清除定时器
console.log('组件即将卸载');
}
render() {
return <div>{this.state.count}</div>;
}
}
Hooks 对应关系:
javascript
function MyComponent() {
const [count, setCount] = useState(0);
// componentDidMount
useEffect(() => {
console.log('组件挂载完成');
}, []); // 空依赖数组 = 只执行一次
// componentDidUpdate (count 变化时重渲染)
useEffect(() => {
console.log('count 变化了');
}, [count]);
// componentWillUnmount
useEffect(() => {
return () => {
console.log('组件即将卸载');
};
}, []);
// 正常写法:
useEffect(() => {
console.log('挂载或 count 变化');
return () => {
console.log('清理');
};
}, [count]);
return <div>{count}</div>;
}
实战案例:WebSocket 订阅:
javascript
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// 订阅,创建实时通信长连接
const ws = new WebSocket(`ws://api.com/room/${roomId}`);
//收到后端推送的消息,把消息追加到页面
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
// 清理函数:roomId 变化或组件卸载时执行
return () => {
ws.close();
console.log(`断开房间 ${roomId}`);
};
}, [roomId]); // roomId 变化时,先执行清理,再重新订阅
return (
<ul>
{messages.map((msg, i) => <li key={i}>{msg}</li>)}
</ul>
);
}
类组件生命周期:
- 阶段式:挂载 → 更新 → 卸载
- 更新全跑,不精准
- 清理只在卸载
Hooks 生命周期:
- 响应式:依赖驱动
- 精准控制执行时机
- 每次更新前都会清理
- 代码更简洁、逻辑更内聚
WebSocket + useEffect:
- 建立实时连接
- 接收实时消息
- 依赖变化自动重连
- 组件卸载自动断开
- 完美避免内存泄漏
4.并发请求:
| 场景 | 叫法 | 代码方案 |
|---|---|---|
| 多个请求互不依赖,要快 | 并行 | Promise.all |
| 请求之间有先后依赖 | 串行 | .then() 链式 |
| 频繁触发,会错乱 | 竞态 | 标志位 / AbortController |
| 统称 | 并发 | 以上全部 |
场景 1:多个独立请求并行
javascript
function Dashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
useEffect(() => {
// ✅ 并发请求,不用等待
Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]).then(([userData, postsData, commentsData]) => {
setUser(userData);
setPosts(postsData);
setComments(commentsData);
});
}, []);
return <div>...</div>;
}
场景 2:依赖请求(串行)
javascript
function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
// 先获取用户信息,再获取帖子
fetch(`/api/user/${userId}`)
.then(r => r.json())
.then(user => {
return fetch(`/api/posts?author=${user.name}`);
})
.then(r => r.json())
.then(setPosts);
}, [userId]);
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
场景 3:竞态条件处理
标志位法:
javascript
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let ignore = false; // 标志位
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
if (!ignore) { // 只处理最新请求
setResults(data);
}
});
return () => {
ignore = true; // 清理时标记为忽略
};
}, [query]);
return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>;
}
更好的方案:AbortController
Kotlin
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => controller.abort(); // 取消旧请求
}, [query]);