jtwang7 / React-Note

React 学习笔记
8 stars 2 forks source link

React 填坑记录 - 相同组件复用时状态无法独立存储的问题 #37

Open jtwang7 opened 2 years ago

jtwang7 commented 2 years ago

问题场景描述

项目中引入了两个 ECharts 图表,由于两个图标逻辑样式等类似,因此写成了一个公用的函数组件,通过传入不同的 props 来制定出不同的 ECharts 图表。按理来说,两个图表之间的状态是互不影响的 (React 本身就是这么设计的,每个函数组件都有自己独立的作用域),但是,在传入不同 props 的时候发现:始终渲染的是后一个创建的图表 ?!

原因

起初认为是因为没有设置 key 值,导致 React 内部优化实现了组件的复用,但添加 key 值后并没有什么改变,React key 只在数组上下文中生效。 后来想到,React 组件内部的状态是独立的,这一点毋庸置疑,相同组件的复用,其状态在一般情况下是互不影响的,因为它由组件实例(函数组件为例)的作用域持有。除非!你把这个状态变量定义在了函数组件外部!React 在每次 props 状态改变的时候,都会重新执行(函数)组件的生成函数,若状态被定义在外部,那么复用的组件在执行生成函数时,会因为闭包导致持有外部的状态变量(上一个组件的部分状态),从而出错。

代码实例

错误代码

import ...

let myChart = null; // 错误的做法,组件复用时,TimerCommonBar 函数会持有这个闭包变量
export default function TimerCommonBar(props) {
  const ref = useRef(null);

  // init echarts
  useEffect(() => {
    myChart= echarts.init(ref.current);
    myChart.setOption(option)
    return () => {
      myChart.dispose();
    }
  }, [])

  useEffect(() => {
    option.series[0].data = data
    myChart.setOption(option)
  }, [data])

  return (
    <div ref={ref} className={`timer-line-${type}bar`}></div>
  )
}

假设用上述代码创建了两个组件,创建第一个组件时 myChart = null,创建完成后 myChart = 1;创建第二个组件时 myChart = 1,创建完成后 myChart = 2;然后我们修改第一个组件,由于实例已经存在,所以不会再改变 myChart,但是此时 myChart = 2,导致我们以为修改的是第一个组件,实际改变了第二个组件。

函数组件的外部变量被复用了,没有相互独立。

正确代码

import ...

export default function TimerCommonBar(props) {
  const ref = useRef(null);
  const myChart = useRef(null); // 正确做法,将状态以 React 规范存储在函数内部,React 会在重复调用组件生成函数时,独立持有这一状态.

  // init echarts
  useEffect(() => {
    myChart.current = echarts.init(ref.current);
    myChart.current.setOption(option)
    return () => {
      myChart.current.dispose();
    }
  }, [])

  useEffect(() => {
    option.series[0].data = data
    myChart.current.setOption(option)
  }, [data])

  return (
    <div ref={ref} className={`timer-line-${type}bar`}></div>
  )
}

总结

React 函数组件利用了函数作用域的性质,使得它内部的状态变量会互相隔离,不会被影响。但是定义在函数组件外部的变量没有被隔离,导致函数组件复用时状态混乱。 出于安全和代码可维护性考虑,函数组件外部最好不要定义任何'变量'(这里指的是let和var定义 并且后续会修改改变量), 只应该使用 import 导入,或者 const 定义常量。 我们之所以在有些情况下将变量定义到函数组件外部,是因为每次更新都会重新执行函数组件,导致函数组件内一些永不改变的变量被反复的创建,影响性能。不是说变量定义到函数组件外部是错误的,我们应该遵循以下原则: