React中useRef与组件外部变量的差异:为何后者非最佳实践?
先看你给出的两段代码:
第一段(useRef实现):
function MyComponent(){ const isPersistent = useRef(true) // some more code... }
第二段(模块级let):
let isPersistent = true function MyComponent(){ // some more code... }
核心差异
组件实例隔离性:
useRef创建的变量是每个组件实例独有的——每渲染一个MyComponent,就会生成一个独立的ref实例,彼此完全隔离。而模块级的let isPersistent是整个模块共享的,所有MyComponent实例都会读写同一个变量,修改它会影响所有组件实例的逻辑。生命周期绑定关系:
useRef的生命周期和组件实例完全绑定:组件挂载时创建,卸载后会随组件实例一起被垃圾回收。模块级变量则是模块加载时创建,只要模块不被卸载,变量就会一直存在,哪怕所有MyComponent实例都已经被销毁,它的状态依然会残留。React机制兼容性:
useRef属于React Hooks体系,虽然它的变化不会触发组件重渲染,但它依然处于React的管控范围内,能适配React的并发渲染、Suspense等特性。模块级变量完全脱离React的控制,在复杂渲染场景下可能出现状态和组件渲染不一致的问题。
为什么模块级变量不是最佳实践
哪怕不关联DOM元素,这种写法也存在诸多问题:
实例污染风险:如果页面上同时存在多个MyComponent实例,它们会共享同一个
isPersistent变量。比如其中一个实例修改了isPersistent = false,其他所有实例的逻辑都会受到影响,这几乎都是不符合业务预期的。内存泄漏隐患:如果这个模块级变量引用了组件内的函数、DOM节点或者其他和组件实例绑定的对象,当组件卸载后,变量依然持有这些引用,导致这些对象无法被垃圾回收,长期运行会造成内存泄漏。
调试与维护成本高:模块级变量的变化不会被React DevTools追踪,出现问题时你很难定位到底是哪个组件实例修改了它。而useRef的值可以在DevTools中直接查看每个组件实例的状态,调试和维护更清晰。
并发模式下的不可预期性:在React并发模式中,组件的渲染可能被中断、暂停或复用,模块级变量的状态变化没有和组件的渲染周期绑定,很容易出现组件逻辑和变量状态不一致的诡异问题,而useRef则能和React的渲染机制协同工作,避免这类问题。
内容的提问来源于stack exchange,提问作者Ido Kadosh

