React Native计算器按钮重复渲染优化问题求助
问题描述
在React Native开发计算器时,按钮按下到数值显示存在明显延迟。核心原因是每次输入触发calculation状态变更,导致整个Calculator组件重渲染,所有按钮随之重渲染,加剧了延迟。
性能分析显示,按钮重渲染是因为onPress绑定的handleSymbolPress函数每次都会变化。尝试过用useCallback包裹该函数,但由于依赖calculation(每次状态更新都会变化),无法阻止函数重新创建;给单个按钮的onPress用空依赖useCallback会导致无法获取最新的calculation状态,输入值被覆盖;添加calculation为依赖则回到所有按钮重渲染的问题。
原代码如下:
import { Dimensions, StyleSheet, TextInput, View } from 'react-native'; import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { evaluate } from 'mathjs'; import { defaultStyles } from '../config/styles'; import { Button } from 'react-native'; const { width: deviceWidth, height: deviceHeight } = Dimensions.get('window'); function Calculator({}) { console.log('MainCalculator rendered'); const [calculation, setCalculation] = useState('0'); const handleSymbolPress = useCallback( (symbol) => { console.log('calculation:', calculation, symbol); switch (symbol) { case '+': case '-': case '*': case '/': if ( calculation.endsWith('+') || calculation.endsWith('-') || calculation.endsWith('*') || calculation.endsWith('/') ) { setCalculation(calculation.slice(0, -1) + symbol); } else { setCalculation(calculation + symbol); } break; case '.': if (!calculation.includes('.')) { setCalculation(calculation + symbol); } break; case 'C': setCalculation('0'); break; case '=': try { const result = evaluate(calculation); setCalculation(String(result)); } catch (error) { console.log(error); } break; default: if (calculation === '0') { setCalculation(symbol); } else { setCalculation(calculation + symbol); } break; } }, [calculation] ); return ( <View style={styles.container}> <View style={styles.calculationContainer}> <TextInput style={styles.appMainCalculation} value={calculation} onChangeText={setCalculation} showSoftInputOnFocus={false} caretHidden={false} /> </View> <View style={styles.extraPadButton}> <Button title='C' onPress={() => handleSymbolPress('C')} /> <Button title='(' onPress={() => handleSymbolPress('(')} /> <Button title=')' onPress={() => handleSymbolPress(')')} /> <Button title='/' onPress={() => handleSymbolPress('/')} /> </View> <View style={styles.padButtonsContainer}> <View style={styles.padButtonRow}> <Button title='7' onPress={() => handleSymbolPress('7')} /> <Button title='8' onPress={useCallback( () => handleSymbolPress('8'), [calculation] )} /> <Button title='9' onPress={() => handleSymbolPress('9')} /> <Button title='*' onPress={() => handleSymbolPress('*')} /> </View> <View style={styles.padButtonRow}> <Button title='4' onPress={() => handleSymbolPress('4')} /> <Button title='5' onPress={() => handleSymbolPress('5')} /> <Button title='6' onPress={useCallback(() => handleSymbolPress('6'))} /> <Button title='-' onPress={() => handleSymbolPress('-')} /> </View> <View style={styles.padButtonRow}> <Button title='1' onPress={() => handleSymbolPress('1')} /> <Button title='2' onPress={() => handleSymbolPress('2')} /> <Button title='3' onPress={() => handleSymbolPress('3')} /> <Button title='+' onPress={() => handleSymbolPress('+')} /> </View> <View style={styles.padButtonRow}> <Button title='0' onPress={() => handleSymbolPress('0')} /> <Button title='.' onPress={() => handleSymbolPress('.')} /> <Button title='=' onPress={() => handleSymbolPress('=')} /> </View> </View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', // backgroundColor: defaultStyles.colors.white, paddingBottom: deviceHeight * 0.05, }, calculationContainer: { width: deviceWidth * 0.9, alignItems: 'flex-end', marginBottom: deviceHeight * 0.02, }, appMainCalculation: { textAlign: 'right', fontSize: deviceHeight * 0.07, fontWeight: 'bold', width: '100%', }, appMainCalculationText: { color: defaultStyles.colors.dark, }, extraPadButton: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: deviceHeight * 0.02, width: deviceWidth * 0.9, }, padButtonsContainer: { width: deviceWidth * 0.9, }, padButtonRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: deviceHeight * 0.02, }, }); export default Calculator;
解决方案
1. 使用setCalculation的函数式更新,移除calculation依赖
React的状态更新函数支持传入回调函数,该函数接收当前最新状态值作为参数,这样无需在useCallback依赖数组中加入calculation,保证handleSymbolPress函数引用稳定。
修改后的handleSymbolPress:
const handleSymbolPress = useCallback( (symbol) => { setCalculation(prevCalculation => { console.log('calculation:', prevCalculation, symbol); switch (symbol) { case '+': case '-': case '*': case '/': if ( prevCalculation.endsWith('+') || prevCalculation.endsWith('-') || prevCalculation.endsWith('*') || prevCalculation.endsWith('/') ) { return prevCalculation.slice(0, -1) + symbol; } else { return prevCalculation + symbol; } case '.': if (!prevCalculation.includes('.')) { return prevCalculation + symbol; } return prevCalculation; case 'C': return '0'; case '=': try { const result = evaluate(prevCalculation); return String(result); } catch (error) { console.log(error); return prevCalculation; } default: if (prevCalculation === '0') { return symbol; } else { return prevCalculation + symbol; } } }); }, [] // 空依赖数组,函数引用不再变化 );
2. 封装Memoized按钮组件,避免不必要重渲染
React Native原生Button未做记忆化处理,父组件重渲染时会跟着更新。我们封装一个用React.memo包裹的按钮组件:
import { Button as RNButton } from 'react-native'; import React from 'react'; const MemoizedButton = React.memo(({ title, onPress }) => { console.log(`Button ${title} rendered`); // 验证重渲染情况 return <RNButton title={title} onPress={onPress} />; });
在Calculator组件中替换所有原生Button为MemoizedButton:
// 替换导入语句 // import { Button } from 'react-native'; // 渲染部分示例 <MemoizedButton title='C' onPress={() => handleSymbolPress('C')} /> <MemoizedButton title='(' onPress={() => handleSymbolPress('(')} /> // 其他按钮同理替换
3. 优化按钮onPress绑定(可选)
虽然handleSymbolPress已稳定,但每个按钮的() => handleSymbolPress('X')仍会在父组件重渲染时创建新函数。可以修改MemoizedButton,通过symbol属性传递参数,内部绑定稳定的回调:
import { Button as RNButton } from 'react-native'; import React, { useCallback } from 'react'; const MemoizedButton = React.memo(({ title, symbol, onPress }) => { const handlePress = useCallback(() => { onPress(symbol); }, [onPress, symbol]); return <RNButton title={title} onPress={handlePress} />; });
使用方式调整为:
<MemoizedButton title='C' symbol='C' onPress={handleSymbolPress} /> <MemoizedButton title='(' symbol='(' onPress={handleSymbolPress} /> // 其他按钮同理
效果验证
修改完成后,仅被点击的按钮会触发重渲染(可通过console.log验证),Calculator组件重渲染时其他按钮不会跟随更新,输入延迟问题会得到明显改善。
内容的提问来源于stack exchange,提问作者mammothestate

