React Hook原理速通笔记1(useEffect 原理、使用踩坑、渲染周期、依赖项)

大家好,我是此林。

今天来记录下 useEffect Hook 的学习笔记。

useEffect 干嘛的?

useEffect 是 React Hooks 里用来处理副作用的。

作用:
组件渲染完之后,要做点"额外的事",就会用到 useEffect

说到浏览器渲染,就不得不说下 React 的完整渲染周期和 Hook 执行时机了。

可以看到,useEffect 在浏览器绘制之后才执行。


什么叫"副作用"?

不是"坏事",而是不直接参与渲染 UI 的操作,比如:

  • 请求接口(fetch / axios)

  • 操作 DOM

  • 监听事件(scroll、resize)

  • 定时器(setInterval / setTimeout)

  • 本地存储(localStorage)

  • 订阅 / 取消订阅


基本用法

复制代码
useEffect(() => {
  // 副作用代码
}, [依赖]);

1. 每次渲染后都执行

复制代码
useEffect(() => {
  console.log('组件渲染了');
});

不写依赖数组

  • 初次渲染执行

  • 每次 state / props 改变都会执行

这个因为再每次 state 状态改变后都会重新执行,通常不是我们想要的,可能会引发 bug 和性能浪费,写代码的时候要谨慎。


2. 只在组件挂载时执行(最常见)

复制代码
useEffect(() => {
  fetchData();
}, []);

依赖数组是空 []

等价于 class 组件的:

复制代码
componentDidMount()

常用于:

  • 请求接口(组件挂载后初始化获取API数据)

  • 初始化逻辑


3. 依赖某个 state / props 改变时执行

复制代码
useEffect(() => {
  console.log('count 变了', count);
}, [count]);

只有 count 改变时才执行

等价于:

复制代码
componentDidUpdate(prevProps, prevState)

4. 清理副作用(非常重要)

复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log('running');
  }, 1000);

  return () => {
    clearInterval(timer);
  };
}, []);

可以看到这是个定时器,首先

useEffect(..., [])

  • [] 表示 只在组件第一次渲染(挂载)时执行

  • 不会因为 state 更新再执行

setInterval

const timer = setInterval(() => { console.log('running'); }, 1000);

1 秒 执行一次

控制台一直打印 running
return () => {
clearInterval(timer);
};
这是 清理函数(cleanup):会组件卸载时执行

或 effect 重新执行前执行(这里不会重新执行)

作用:防止定时器泄漏

return 的函数等价于:

复制代码
componentWillUnmount()

对于 React 的 Class API,虽然 class 组件从来没有被正式废弃(deprecated)

从 React 16.8(2019)开始已经不再是推荐写法

官方态度是:

  • 新功能只围绕 Hooks 设计

  • 文档默认全部用函数组件

  • class API 只维护,不增强


useEffect 原理

之前说到 useEffect 有依赖项,那 React 是如何判断依赖项发生了变化呢?

不过要注意一点,因为React使用的是浅比较,如果依赖项传入的是对象,那么React只会比较内存引用,而不会比较内部字段属性。


常见坑

一、依赖项漏写或写错

坑 1:忘记写依赖数组

复制代码
useEffect(() => {
  fetchData()
})

问题

  • 每次 render 都会执行

  • 造成 死循环(特别是 effect 里 setState)

正确

复制代码
useEffect(() => {
  fetchData()
}, [])

[] 表示 只在组件挂载时执行一次


坑 2:依赖项漏写(最常见)

复制代码
useEffect(() => {
  console.log(count)
}, [])

问题

  • count 更新了,但 effect 用的还是 旧值(闭包陷阱)

正确

复制代码
useEffect(() => {
  console.log(count)
}, [count])

原则:effect 中用到的外部变量,都应该出现在依赖数组中


二、闭包陷阱

闭包问题,在定时器/异步场景下出现的比较多,需要特别注意。

坑 3:setInterval / setTimeout 用了旧状态

复制代码
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1)
  }, 1000)
  return () => clearInterval(timer)
}, [])

问题

  • count 永远是初始值

方案 1:函数式更新(推荐)

复制代码
setCount(c => c + 1)

方案 2:把依赖加进去(不适合 interval)

复制代码
}, [count])

这些场景都有一个共同点:

  • useEffect([]) 只创建一次

  • 回调函数 不会随 render 更新

  • 回调只能看到 第一次 render 的变量

典型场景包括:

  • setInterval

  • setTimeout

  • addEventListener

  • WebSocket 回调

  • Promise.then

只要回调"活得比一次 render 久",就要小心闭包


三、无限循环问题

坑 4:依赖里放了 effect 内部修改的 state

复制代码
useEffect(() => {
  setCount(count + 1)
}, [count])

结果

  • render → effect → setState → render → ... 无限循环

正确

  • 明确「什么时候应该触发 effect」

  • 或用条件限制

    useEffect(() => {
    if (count < 10) {
    setCount(c => c + 1)
    }
    }, [count])


四、对象 / 数组作为依赖

坑 5:依赖对象 / 数组导致频繁触发

复制代码
useEffect(() => {
  doSomething()
}, [{ a: 1 }])

问题

  • 每次 render 都是新对象

  • effect 每次都执行

正确(使用 useMemo 缓存对象)

复制代码
const obj = useMemo(() => ({ a: 1 }), [])
useEffect(() => {
  doSomething()
}, [obj])

或直接拆成基础类型:

复制代码
useEffect(() => {
  doSomething()
}, [a])

还有一种方法,就是移到函数组件外部,此时他作为一个常量,永远不会变。

关于这个点,我们之前在 useEffect 原理已经说过了,React 底层使用的是浅比较。


五、函数作为依赖

坑 6:函数每次 render 都变

复制代码
useEffect(() => {
  handler()
}, [handler])

问题

  • 普通函数在 render 时会重新创建

正确

复制代码
const handler = useCallback(() => {
  ...
}, [])

useEffect(() => {
  handler()
}, [handler])

useCallback 和 useMemo 很像,只不过 useMemo 缓存的值,useCallback 缓存的是函数本身。


六、清理副作用没写

坑 7:忘记 cleanup

复制代码
useEffect(() => {
  window.addEventListener('resize', onResize)
}, [])

问题

  • 内存泄漏

  • 事件重复绑定

正确

复制代码
useEffect(() => {
  window.addEventListener('resize', onResize)
  return () => {
    window.removeEventListener('resize', onResize)
  }
}, [])

七、useEffect ≠ 生命周期完全等价

坑 8:把所有逻辑都塞进 useEffect

复制代码
useEffect(() => {
  setState(deriveFromProps(props))
}, [props])

问题

  • 不必要的 render

  • 可读性差

正确思路

  • 能在 render 期间计算的,不要放 effect

    const derived = useMemo(() => {
    return deriveFromProps(props)
    }, [props])


八、React 18 + StrictMode 特殊坑

坑 9:开发环境 effect 执行两次

复制代码
useEffect 执行了 2 次?

原因

  • React 18 StrictMode 故意 double invoke

  • 只在开发环境

解决

  • 不要依赖「只执行一次的副作用逻辑」

  • 所有 effect 都应是 可安全重复执行的

总结 useEffect 三问:

  1. 什么时候执行?(依赖是谁)

  2. 用了哪些外部变量?(依赖全写)

  3. 有没有副作用需要清理?(return)


之前我们说了 React 渲染流程,再看看这张图。

注意这里 useEffect 是在浏览器绘制后异步执行的,这样做有什么好处呢?

不阻塞浏览器绘制,用户响应更快,适合大多数副作用场景(数据获取API调用、事件监听器、订阅管理、日志记录等)

不过 React 里还有个类似的 Hook,它叫 useLayoutEffect。

useLayoutEffect?

由上表可知,useLayoutEffect 在浏览器绘制之前同步执行,可能会导致卡顿,我们用的比较少。

绝大多数副作用场景优先使用 useEffect,仅在有视觉问题等特殊场景才使用 useLayoutEffect。

经典的闪烁问题

下面来理解下 useLayoutEffect 存在的意义。

如果我们使用 useEffect,因为它是在浏览器绘制后才异步执行的,

那么 React 会先渲染一个宽度为 0 的 div,浏览器把宽度为 0 的 div 绘制到屏幕上;

然后 useEffect 才执行把宽度改为 100px,浏览器重新绘制。

这就有可能导致用户肉眼看到 div 从 0 闪到 100px 的过程,体验感差。

所以我们使用 useLayoutEffect 解决这个问题,

首先 React 依然会先渲染一个宽度为 0 的 div,但是在交给浏览器绘制之前,useLayoutEffect 会同步地立刻把 div 宽度改为 100px,从而避免了视觉闪烁问题。

相关推荐
克里斯蒂亚诺更新1 天前
vue3使用pinia替代vuex举例
前端·javascript·vue.js
冰暮流星1 天前
javascript赋值运算符
开发语言·javascript·ecmascript
Chris_12191 天前
Halcon学习笔记-Day5
人工智能·笔记·python·学习·机器学习·halcon
日更嵌入式的打工仔1 天前
Ehercat代码解析中文摘录<7>
笔记·ethercat
悠哉悠哉愿意1 天前
【嵌入式学习笔记】AD/DA
笔记·单片机·嵌入式硬件·学习
西凉的悲伤1 天前
html制作太阳系行星运行轨道演示动画
前端·javascript·html·行星运行轨道演示动画
低保和光头哪个先来1 天前
源码篇 实例方法
前端·javascript·vue.js
你真的可爱呀1 天前
自定义颜色选择功能
开发语言·前端·javascript
小王和八蛋1 天前
JS中 escape urlencodeComponent urlencode 区别
前端·javascript