React中为何输出旧props?解构props时首次数值不一致原因
问题根源:闭包捕获的是render时刻的props
你遇到的核心问题其实是JavaScript闭包特性结合React类组件更新机制导致的,咱们一步步拆解逻辑:
- App组件初始化时
state.number是0,首次渲染会把number=0传给Child组件; - Child组件执行
render时,你解构出const { number } = this.props,这个number是当前render时刻的props快照(也就是0),同时你在render里创建了一个setTimeout定时器; - 1秒后App组件通过
setState把number改成9,触发自身重新渲染,进而Child也会跟着重新渲染——此时Child的props.number变成9,新的render里又会解构出number=9,同时可能又创建了一个新的定时器(你的代码没写完,但逻辑应该是这样)。
那为什么会出现“两个数值不一致”?因为第一个定时器是在Child第一次render时创建的,它的回调函数捕获的是第一次render时的props(number=0);当App更新后,Child第二次render创建的定时器捕获的是第二次的props(number=9)。如果这两个定时器先后触发,你就会看到输出0和9,看起来像是“数值不一致”。
为什么会输出之前的props?
这完全是闭包的锅:类组件的每一次render都会生成一个独立的函数作用域,作用域里的变量(包括解构出来的number、this.props)都是当前render时刻的快照。当你在render里创建异步操作(比如setTimeout),这个异步操作的回调函数会“抓住”当前作用域里的变量——哪怕之后组件更新、props变化,回调里拿到的依然是捕获时的旧值。
给你补全Child组件的代码,看得更清楚:
class Child extends React.Component { render() { const { number } = this.props; // 这个定时器会捕获当前render时刻的number setTimeout(() => { console.log(number); // 第一次render时输出0,第二次render时输出9 }, 2000); return <div>{number}</div>; } }
当App在1秒后更新number到9,Child会立刻重新渲染,此时第二个定时器捕获的是9;而第一个定时器是第一次render时创建的,它回调里的number还是0,所以2秒后会先后输出0和9(顺序取决于定时器触发时间)。
解决方案:拿到最新的props
如果你想在异步回调里获取最新的props,有两种常用方案:
直接访问组件实例的props:不要在render里解构props,而是在回调里直接用
this.props——因为this是组件实例的引用,它的props会实时更新:class Child extends React.Component { render() { setTimeout(() => { console.log(this.props.number); // 永远拿到最新的props值 }, 2000); return <div>{this.props.number}</div>; } }注意:如果是普通函数回调,需要手动绑定
this(比如用bind(this)),箭头函数会自动绑定组件实例。改用函数组件+useRef:如果是函数组件,可以用
useRef保存最新的props,因为ref的current属性是可变的:import { useRef, useEffect } from 'react'; function Child(props) { const propsRef = useRef(props); // 每次props更新时同步到ref useEffect(() => { propsRef.current = props; }, [props]); useEffect(() => { setTimeout(() => { console.log(propsRef.current.number); // 拿到最新的props }, 2000); }, []); return <div>{props.number}</div>; }
总结
你看到的“之前的props”其实是闭包捕获的render时刻的props快照,这不是React的bug,而是JavaScript闭包的正常特性。只要理解每一次render都是一个独立的作用域,异步回调会捕获当前作用域的变量,就能搞懂这个现象的本质啦。
内容的提问来源于stack exchange,提问作者kaijun

