React未用useCallback包裹的事件处理器取值与重渲染问题
React 普通事件函数状态获取问题解析
示例代码如下:
import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import React, { useState, useEffect, useMemo, useRef, useCallback } from "react"; const App = () => { const [channel, setChannel] = useState(null); const handleClick1 = useCallback(() => { console.log(channel); }, [channel]); const handleClick2 = () => { console.log(channel); }; const parentClick = () => { console.log("parent is call"); setChannel((prev) => prev + 1); }; // useEffect(() => { // setChannel(1); // }); return ( <div className="App"> <button onClick={parentClick}>parent click</button> <br /> <br /> <button onClick={handleClick1}>child click1</button> <button onClick={handleClick2}>child click2</button> </div> ); }; const rootElement = document.getElementById("root"); const root = createRoot(rootElement); root.render(<App />);
上述代码中定义了两个按钮点击事件处理函数:handleClick1通过useCallback包裹并将channel加入依赖数组,handleClick2未做Hook包裹。点击父按钮触发channel状态更新后,两个点击处理函数都能获取到正确的channel状态值,针对handleClick2的两个疑问解答如下:
问题1:channel状态值变化是否会触发该函数重新求值,进而导致整个UI重渲染?
答案是否定的,这里完全搞反了因果顺序:
- 触发UI重渲染的唯一原因是
setChannel执行后更新了channel状态,和handleClick2没有关系。React里触发组件重渲染的场景只有三类:组件自身状态更新、父组件重渲染、订阅的context值更新,重渲染本质就是把组件函数从头到尾重新执行一遍。 handleClick2被重新创建(也就是问题里说的重新求值),只是组件重渲染过程中自然产生的结果,根本不是重渲染的诱因。按照JS的基础执行逻辑,函数每次运行时,内部定义的普通变量、普通函数都会重新生成一遍,组件函数也遵循这个规则。
问题2:handleClick2是如何获取到正确的channel状态值的?
核心逻辑非常简单,每一次组件渲染都有属于自己的独立闭包:
- 每一轮组件重渲染执行时,都会生成当前渲染周期专属的
channel值,这个值在当前这轮渲染的整个生命周期里都是固定不变的。 - 这轮渲染里新定义的
handleClick2,本身就是在当前组件函数的执行上下文中创建的,它的闭包捕获的自然就是当前这轮渲染对应的最新channel值。 - 最终渲染到页面上的按钮,
onClick属性绑定的就是这轮渲染新生成的handleClick2,点击按钮时调用的就是这个新函数,当然能拿到最新的状态值。
补充说明:很多人对
useCallback存在误解,觉得它是用来解决闭包拿不到最新值的问题,这完全是错误认知。useCallback的核心作用是缓存函数引用——只要依赖项不变,它就会返回和上一轮渲染完全相同的函数对象,避免把函数作为props传给做了引用相等判断的子组件时,因为每次渲染生成新函数引用导致子组件做不必要的重渲染。反过来如果给useCallback漏传了依赖,缓存的旧函数闭包捕获的是老状态,反而会出现拿不到最新值的bug。
内容的提问来源于stack exchange,提问作者william007
相关产品推荐
相关产品推荐

