hooks的来龙去脉之useEffect,useMemo,useRef

4 阅读7分钟

React 组件天生只干这件事:数据变 → 重新画页面

useEffect 处理的 = 本职之外的额外操作:请求接口、定时器、弹窗、操作 DOM

一、useEffect 是什么意思?

先拆单词:

  • Use:使用
  • Effect副作用(这里是关键)

合起来:使用副作用


二、什么叫 “副作用”?

你写 React 组件时,正常工作是:

接收数据 → 渲染页面(展示内容)

这是它的本职工作

function User({ name, age }) { // 👇 这里就是:接收数据(name、age) 
// 👇 下面 return 就是:渲染页面(展示内容) 
return ( <div> <p>姓名:{name}</p> <p>年龄:{age}</p> </div> ); }
  • 接收数据nameage
  • 渲染页面:return 里的 HTML

但有些事情不属于渲染本身,比如:

  • 打开页面就发请求拿数据
  • 定时器、延时
  • 订阅、监听
  • 操作 DOM
  • 本地存储(localStorage)

这些 “额外做的事”,就叫副作用

一句话总结: useEffect 就是 React 里专门用来做 “额外事情” 的钩子。


三、为什么要叫这个名字?

因为:

  1. React 函数组件本身只负责渲染
  2. 你要做额外操作(副作用) ,就必须用专门的钩子
  3. 所以叫 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 的核心作用(一句话)

存东西、跨渲染保留值,而且不改视图

它只干两件事:

  1. 存数据(像变量,但不丢)
  2. 拿真实 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. 用最直白的堆栈话总结

闭包陷阱的堆栈真相:

  1. 第一次渲染产生栈 A
  2. 定时器回调锁死栈 A(不销毁)
  3. 后续渲染产生栈 B、C、D…… 全新独立栈
  4. 旧回调只能访问栈 A,看不到其他栈
  5. 所以永远打印旧 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 读写

  • 两大用途:

    1. 存定时器、ID、状态标记(不影响视图)
    2. 拿真实 DOM 元素(input、div、canvas…)