React-D3组件中重复定义常量与工具函数的优化问询
我正在开发一个React-D3组件,目前遇到了需要反复定义常量与工具函数的问题。我的组件大致结构如下:
class App extends React.Component { constructor(props) { super(props); this.state = { chartType: 'A' } } getHitData() {...} drawChartA() {...} drawChartB() {...} drawChartC() {...} drawChartD() {...} drawChartE() {...} toggleButton() {...} componentDidMount() { this.drawChartA() } componentDidUpdate() { const { chartType } = this.state; if(chartType == "A") { this.drawChartA() } else if(..B) {...} else if(..C) {...} ... } render() { return ( <div> <svg className='chart'> <g className='chartA' /> <g className='chartB' /> <g className='chartC' /> <g className='chartD' /> </svg> </div> ) } }
在实际代码中,每个drawChartA()、drawChartB()等函数里,我都需要重新定义chartWidth、chartHeight、padding、margin等常量,以及colorScales、xScale、yScale、radiusScale等重要的D3工具函数,这明显违反了DRY原则。
我想请教:chartWidth、padding这类常量是否应该存入this.state?这些D3工具函数该如何处理?我认为它们不适合作为drawChart()函数的参数。希望能得到相关建议,感谢!
解决方案
一、常量的处理:别放state,用静态属性/外部常量/计算属性
首先明确:chartWidth、padding这类值如果不是需要随用户交互或业务逻辑动态变化的状态,就完全没必要放进this.state。state应该只存储驱动组件重新渲染的可变数据,固定或依赖外部条件(比如容器尺寸、props)的常量可以这么处理:
固定不变的常量:直接定义在组件外部,所有draw函数共享:
// 组件外部定义全局常量 const CHART_SETTINGS = { width: 800, height: 500, margin: { top: 20, right: 30, bottom: 40, left: 50 }, padding: 10 }; class App extends React.Component { // ... 组件逻辑 }需要动态计算的尺寸:比如依赖父容器的实际宽度,写一个类的getter方法或者单独的工具函数来计算,避免重复写:
class App extends React.Component { getChartDimensions() { const container = document.querySelector('.chart-container'); const containerWidth = container.offsetWidth; return { width: containerWidth - CHART_SETTINGS.margin.left - CHART_SETTINGS.margin.right, height: 500 - CHART_SETTINGS.margin.top - CHART_SETTINGS.margin.bottom, margin: CHART_SETTINGS.margin }; } drawChartA() { const { width, height, margin } = this.getChartDimensions(); // 直接使用计算后的尺寸 } }
二、D3工具函数:封装成复用方法或实例属性
那些colorScales、xScale这类依赖尺寸和数据的工具函数,完全可以封装成一个复用方法,不用在每个draw函数里重新定义:
1. 封装成getScales方法
写一个返回所有scale的方法,接收数据和尺寸作为参数(如果数据是全局可用的,也可以直接从state/props取):
class App extends React.Component { getScales(data, dimensions) { const { width, height } = dimensions; return { xScale: d3.scaleLinear() .domain([0, d3.max(data, d => d.x)]) .range([0, width]), yScale: d3.scaleBand() .domain(data.map(d => d.name)) .range([height, 0]) .padding(0.2), colorScale: d3.scaleOrdinal(d3.schemeCategory10) }; } drawChartA() { const data = this.getHitData(); const dimensions = this.getChartDimensions(); const { xScale, yScale, colorScale } = this.getScales(data, dimensions); // 接下来用这些scale绘制图表 } }
2. 缓存为实例属性(可选)
如果你的数据和尺寸不会频繁变化,可以把计算好的scales存在this.scales里,避免重复计算:
class App extends React.Component { componentDidMount() { this.updateScales(); this.drawChartA(); } componentDidUpdate(prevProps, prevState) { // 当数据或chartType变化时,更新scales if (prevState.chartType !== this.state.chartType || prevProps.data !== this.props.data) { this.updateScales(); } // 绘制对应图表,用对象映射替代冗长的else if const chartDrawers = { A: this.drawChartA, B: this.drawChartB, C: this.drawChartC }; chartDrawers[this.state.chartType]?.call(this); } updateScales() { const data = this.getHitData(); const dimensions = this.getChartDimensions(); this.scales = this.getScales(data, dimensions); } drawChartA() { // 直接使用this.scales.xScale const { xScale, yScale } = this.scales; // ... 绘制逻辑 } }
三、进阶优化:拆分组件,按图表类型封装子组件
从长远来看,把每个图表类型拆成独立的React组件会更易维护,每个子组件负责自己的scale计算和绘制,父组件只负责切换渲染:
// 子组件:ChartA.jsx class ChartA extends React.Component { getScales() { const { data, width, height } = this.props; // 计算A图表专属的scale } componentDidMount() { this.draw(); } componentDidUpdate() { this.draw(); } draw() { const scales = this.getScales(); // D3绘制逻辑 } render() { return <g className="chartA" />; } } // 父组件 class App extends React.Component { state = { chartType: 'A' }; getChartDimensions() { // 计算完整的图表尺寸 const container = document.querySelector('.chart-container'); return { fullWidth: container.offsetWidth, fullHeight: 500, width: container.offsetWidth - CHART_SETTINGS.margin.left - CHART_SETTINGS.margin.right, height: 500 - CHART_SETTINGS.margin.top - CHART_SETTINGS.margin.bottom }; } render() { const { chartType } = this.state; const dimensions = this.getChartDimensions(); const data = this.getHitData(); return ( <div className="chart-container"> <svg width={dimensions.fullWidth} height={dimensions.fullHeight}> {chartType === 'A' && <ChartA data={data} {...dimensions} />} {chartType === 'B' && <ChartB data={data} {...dimensions} />} {/* 其他图表组件 */} </svg> </div> ); } }
这样每个子组件的职责更单一,重复代码也会大幅减少,还能独立测试每个图表组件。
内容的提问来源于stack exchange,提问作者Canovice

