React 组件天生只干这件事:数据变 → 重新画页面
useEffect 处理的 = 本职之外的额外操作:请求接口、定时器、弹窗、操作 DOM
一、useEffect 是什么意思?
先拆单词:
- Use:使用
- Effect:副作用(这里是关键)
合起来:使用副作用。
二、什么叫 “副作用”?
你写 React 组件时,正常工作是:
接收数据 → 渲染页面(展示内容)
这是它的本职工作。
function User({ name, age }) { // 👇 这里就是:接收数据(name、age)
// 👇 下面 return 就是:渲染页面(展示内容)
return ( <div> <p>姓名:{name}</p> <p>年龄:{age}</p> </div> ); }
- 接收数据:
name、age - 渲染页面:return 里的 HTML
但有些事情不属于渲染本身,比如:
- 打开页面就发请求拿数据
- 定时器、延时
- 订阅、监听
- 操作 DOM
- 本地存储(localStorage)
这些 “额外做的事”,就叫副作用。
一句话总结: useEffect 就是 React 里专门用来做 “额外事情” 的钩子。
三、为什么要叫这个名字?
因为:
- React 函数组件本身只负责渲染
- 你要做额外操作(副作用) ,就必须用专门的钩子
- 所以叫 Use + Effect = 使用副作用
四、最简单的例子(一看就懂)
例子 1:页面一打开就弹出提示
import { useEffect } from 'react';
function Home() {
// 组件加载完 → 自动执行这里的代码
useEffect(() => {
alert("页面加载完成啦!");
}, []); // 空数组 = 只执行一次
return <div>首页</div>
}
作用: 组件一渲染,就自动弹框。
例子 2:打开页面发送网络请求(最常用)
import { useEffect, useState } from 'react';
function List() {
const [list, setList] = useState([]);
// 一进页面就发请求拿数据
useEffect(() => {
fetch("https://api.xxx.com/list")
.then(res => res.json())
.then(data => {
setList(data); // 把数据存起来
});
}, []);
return <div>{list.map(item => <p>{item}</p>)}</div>
}
作用: 页面加载 → 自动发请求 → 拿到数据渲染。
五、第二个参数 [] 是什么?
useEffect(() => {
// 代码
}, [依赖]);
[]空数组:只在页面第一次加载时执行一次(最常用)[count]:count 变了,才重新执行- 不写第二个参数:每次渲染都执行(一般不要用)
六、再给你一句终极总结
useEffect = 组件加载 / 更新时,自动帮你执行额外操作的工具。
总结
- useEffect = 使用副作用
- 副作用 = 渲染之外的额外操作(请求、定时器、弹窗等)
- 空数组
[]= 只执行一次(页面刚加载时) - 它是 React 处理异步、请求、定时器最核心的钩子
useEffect 的工作机制:
function Demo() { // 每次渲染,这里全部重新跑一遍
const [count, setCount] = useState(0);
console.log('组件执行了');
useEffect(...) return <div>{count}</div>;
}
-
页面进来 → 执行 Demo () 第一次
-
走到 return → 渲染 DOM
-
渲染完 → 跑 useEffect
-
useEffect 里 setData → 状态变了
-
React 发现状态变 → 再执行 Demo () 第二次
-
再 return → 更新 DOM
一、单词拆开:useMemo 到底啥意思?
- use = 使用
- memo = memoization(记忆化)
合起来:
useMemo = 使用 “记忆” 功能
就这么简单。
二、它是干嘛的?
你已经知道:
组件渲染 = 函数从头到尾重新执行一遍
那如果里面有很耗性能的计算,比如:
function Demo() {
// 每次渲染都重新算一遍!
const 结果 = 超级复杂的计算(数据);
return <div>{结果}</div>
}
每次渲染,不管数据变没变,都要重新算一遍,很浪费。
useMemo 就是:
“记住上次算出来的结果,没必要就别重算”
三、核心一句话
useMemo = 缓存计算结果,避免重复计算。
四、最简单对比例子
不用 useMemo(每次都算)
function Demo() {
const [count, setCount] = useState(0);
// 🔥 每次渲染都重新执行!
const doubleCount = count * 2;
return <div>{doubleCount}</div>
}
用 useMemo(缓存结果)
function Demo() {
const [count, setCount] = useState(0);
// ✅ 只有 count 变了,才重新算
const doubleCount = useMemo(() => {
console.log("计算了!");
return count * 2;
}, [count]); // 依赖:count 变才重算
return <div>{doubleCount}</div>
}
count不变 → 不重新计算count变 → 才重新计算
这就是 useMemo 的作用。
五、和 useEffect 长得像,区别巨大
你看结构很像:
useEffect(() => {}, []);
useMemo(() => {}, []);
但完全不是一个东西:
| 钩子 | 干什么 | 返回值 |
|---|---|---|
| useEffect | 做副作用(请求、定时器…) | 无返回值 |
| useMemo | 做计算,缓存结果 | 返回计算结果 |
一句话区分:
- useEffect:做事
- useMemo:算值并记住
六、真正实用场景(复杂计算)
const [list, setList] = useState([1,2,3,4,5]);
// 只有 list 变,才重新过滤+排序
const expensiveResult = useMemo(() => {
return list
.filter(item => item % 2 === 0)
.sort((a,b) => b - a);
}, [list]);
如果不用 useMemo,组件一渲染就重新过滤排序,大数据量会卡顿。
七、终极总结(你一定能记住)
- useMemo = 记忆计算结果
- 作用:避免重复做昂贵计算
- 依赖数组不变 → 直接用上次的结果,不重新跑函数
- 渲染 = 函数重新执行,但 useMemo 可以让里面的计算不重复执行
一、useRef 名字拆开(为什么这么命名)
- use = 使用
- Ref = Reference(引用、指针)
合起来:
useRef = 使用一个 “引用 / 指针”
就这么简单。
二、什么是 “引用”?
你可以把 useRef 理解成:
一个安全的小盒子
你可以在里面放任何东西
组件反复渲染(函数重新执行)时
它不会丢、不会重置、不会触发重新渲染
对比一下你已经懂的:
| 东西 | 组件重新渲染 | 改了会触发渲染? |
|---|---|---|
| 普通变量 | 会被重置 | 不会 |
| useState | 保留值 | 会触发重渲染 |
| useRef | 保留值 | 不会触发重渲染 |
三、useRef 的核心作用(一句话)
存东西、跨渲染保留值,而且不改视图
它只干两件事:
- 存数据(像变量,但不丢)
- 拿真实 DOM(拿真实元素)
四、最简单例子 1:存个值,不触发渲染
import { useRef, useState } from 'react';
function Demo() {
// 1. 创建一个 ref 小盒子
const countRef = useRef(0);
const [, forceRender] = useState({});
const add = () => {
// 2. 改值:永远用 .current
countRef.current++;
console.log(countRef.current);
// 🔥 注意:这里不会触发组件重新渲染!
};
return (
<div>
<button onClick={add}>+1</button>
<p>ref 值:{countRef.current}</p>
<button onClick={() => forceRender({})}>点我才刷新视图</button>
</div>
);
}
关键点:
- 修改
ref.current不会让组件重新执行 - 值会一直保留,渲染多少次都不丢
原理:
1. JS 最基础的内存规则
- 基本类型(count=0)存在栈里
- 对象、函数存在堆里
- 函数执行 = 创建新栈帧
- 函数执行完 = 栈帧默认销毁
- 但如果栈里的东西还被堆引用 → 栈帧不销毁(这就是闭包)
2. 第一次渲染(关键!)
function Demo() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count); // 这里的 count 来自第一次渲染的栈
}, 1000);
}, []);
}
第一步:执行 Demo ()
- 创建栈帧 A
- 在栈 A 里创建变量:
count = 0(存在栈里)
第二步:执行 useEffect
- 堆里创建定时器回调函数:
() => { console.log(count) } - 这个函数引用栈 A 的 count
第三步:setInterval 把回调函数放在堆里长期保存
- 回调函数 一直活着
- 回调函数 一直引用栈 A
- 栈 A 被锁死,永远不销毁!
这就是闭包的堆栈本质。
3. 第二次渲染(新的栈!)
你点击按钮,setCount 触发重渲染:
Demo () 再次执行
- 创建全新栈帧 B
- 栈 B 里创建新的 count = 1
- 和栈 A 半毛钱关系没有
但定时器还在引用栈 A
定时器函数只认识栈 A 的 count,完全不知道栈 B 存在。
栈 B 执行完会被销毁,定时器碰不到它。
4. 第三次、第四次渲染……
每次都是:
- 新栈 C、栈 D、栈 E……
- 新 count
- 定时器永远只守着栈 A
5. 用最直白的堆栈话总结
闭包陷阱的堆栈真相:
- 第一次渲染产生栈 A
- 定时器回调锁死栈 A(不销毁)
- 后续渲染产生栈 B、C、D…… 全新独立栈
- 旧回调只能访问栈 A,看不到其他栈
- 所以永远打印旧 count
根本原因:
闭包保存的是「当时的栈」,
6. 为什么 useRef 能破?(堆栈角度)
const countRef = useRef(0);
useRef创建一个对象,存在堆里- 整个组件生命周期只有这一个对象
- 所有渲染栈(A、B、C、D)都共享这个堆对象
- 定时器锁住的是这个堆对象
- 每次渲染都修改堆里的
current - 定时器读
current自然永远最新
五、最简单例子 2:拿真实 DOM(最常用)
import { useRef, useEffect } from 'react';
function Demo() {
// 1. 创建 ref
const inputRef = useRef(null);
useEffect(() => {
// 3. 拿到真实 input!
inputRef.current.focus();
}, []);
// 2. 绑到元素上
return <input ref={inputRef} />;
}
作用:拿到原生 DOM,做 JS 才能做的事:focus、scroll、操作 DOM
六、和 useState 最核心区别
- useState:存视图要用的数据 → 改了就刷新页面
- useRef:存内部用的东西 → 改了页面不动
七、终极人话总结
-
useRef = 一个不会丢、不会触发渲染的小盒子
-
必须用 .current 读写
-
两大用途:
- 存定时器、ID、状态标记(不影响视图)
- 拿真实 DOM 元素(input、div、canvas…)