React Native实现Airbnb日历:如何阻止子组件重复渲染提升性能?
问题根源
你的CalendarDayBox虽然用了React.memo,但以下几个原因导致它还是会重复渲染:
handleDateSelect依赖selectedDates状态:每次选中日期变化时,selectedDates更新,handleDateSelect会被重新创建,传递给所有CalendarDayBox的handleDateSelectprop引用改变,触发重渲染。getDayStyles、isWithinRange、isStartOrEndDate均依赖selectedDates:这些函数每次状态更新都会重新生成,传递给子组件的函数引用变化,导致React.memo的浅比较失效。dayStyles是循环内创建的新对象:即使样式内容不变,每次循环都会生成新的对象,React.memo会认为prop变化,触发重渲染。
优化方案
1. 使用函数式更新重构handleDateSelect
去掉对selectedDates的依赖,让函数引用保持稳定:
const handleDateSelect = useCallback((day, month, year) => { const date = format(new Date(year, month, day), 'yyyy-MM-dd'); setSelectedDates(prev => { if (!prev.start) return {...prev, start: date}; else if (!prev.end) return {...prev, end: date}; else return {start: date, end: null}; }); }, []); // 无依赖,引用始终稳定
2. 预计算所有日期的样式和状态数据
将原本在子组件中计算的dayStyles、isWithinRange、isStartOrEndDate结果提前计算好,避免传递函数给子组件,同时保证数据引用稳定:
// 预计算每个日期的样式映射 const dayStylesMap = useMemo(() => { const map = {}; for (let day = 1; day <= daysInMonth; day++) { const date = format(new Date(year, month, day), 'yyyy-MM-dd'); let styles; if (date === selectedDates.start || date === selectedDates.end) { styles = {backgroundColor: 'black', textColor: 'white'}; } else if (selectedDates.start && selectedDates.end) { const isInRange = (date > selectedDates.start && date < selectedDates.end) || (date < selectedDates.start && date > selectedDates.end); styles = isInRange ? {backgroundColor: 'grey', textColor: 'black'} : {backgroundColor: 'transparent', textColor: 'black'}; } else { styles = {backgroundColor: 'transparent', textColor: 'black'}; } map[date] = styles; } return map; }, [selectedDates, year, month, daysInMonth]); // 预计算所有42个格子的完整数据 const dayData = useMemo(() => { const data = []; for (let idx = 0; idx < 42; idx++) { const dayNumber = getDayNumber(idx + 1); if (dayNumber === null) { data.push({ dayNumber: null, isWithinRange: false, startEndStyles: {opacity: 0}, styles: {backgroundColor: 'transparent', textColor: 'black'} }); continue; } const date = format(new Date(year, month, dayNumber), 'yyyy-MM-dd'); const isStart = date === selectedDates.start; const isEnd = date === selectedDates.end; const isWithin = isStart || isEnd || (selectedDates.start && selectedDates.end && ((date > selectedDates.start && date < selectedDates.end) || (date < selectedDates.start && date > selectedDates.end))); let startEndStyles = {opacity: 0}; if (selectedDates.start && selectedDates.end) { if (!isStart && !isEnd) { const startOfRow = [0,7,14,21,28,35].includes(idx); const endOfRow = (dayNumber + dayOffset) % 7 === 0; startEndStyles = { borderTopLeftRadius: startOfRow ? 10 : 0, borderBottomLeftRadius: startOfRow ? 10 : 0, borderTopRightRadius: endOfRow ? 10 : 0, borderBottomRightRadius: endOfRow ? 10 : 0 }; } else { const rangeReversed = selectedDates.start > selectedDates.end; if (isStart && rangeReversed) { startEndStyles = {left:'-10%', borderTopRightRadius:50, borderBottomRightRadius:50}; } else if (isEnd && rangeReversed) { startEndStyles = {left:'10%', borderTopLeftRadius:50, borderBottomLeftRadius:50}; } else if (isStart) { startEndStyles = {left:'10%', borderTopLeftRadius:50, borderBottomLeftRadius:50}; } else if (isEnd) { startEndStyles = {left:'-10%', borderTopRightRadius:50, borderBottomRightRadius:50}; } } } data.push({ dayNumber, isWithinRange: isWithin, startEndStyles, styles: dayStylesMap[date] }); } return data; }, [selectedDates, year, month, daysInMonth, dayOffset, dayStylesMap]);
3. 修改CalendarDayBox接收预计算数据
不再传递函数,直接使用预计算好的结果,确保所有props都是稳定的引用或原始值:
const CalendarDayBox = React.memo( ({dayStyles, month, year, boxWidth, isWithinRange, dayNumber, startEndStyles, handleDateSelect}) => { return ( <View style={{justifyContent: "center", alignItems: "center", flexGrow: 1, flexBasis: boxWidth, height: boxWidth, marginBottom: 1, position: "relative"}}> <View style={{opacity: isWithinRange ? 1 : 0, position: "absolute", backgroundColor: "gray", height: boxWidth - 10, width: `${boxWidth + 1}px`, ...startEndStyles}}></View> <TouchableHighlight disabled={dayNumber === null} onPress={() => handleDateSelect(Number(dayNumber), month, year)}> <View style={{borderRadius: 50, width: boxWidth - 10, height: boxWidth - 10, justifyContent: "center", alignItems: "center", backgroundColor: dayStyles.backgroundColor}}> <Text style={{textAlign: "center", color: dayStyles.textColor, lineHeight: 0, fontSize: 10, fontWeight: "bold"}}>{dayNumber}</Text> </View> </TouchableHighlight> </View> ); } );
4. 更新渲染循环使用预计算数据
{dayData.map((item, idx) => ( <CalendarDayBox key={idx} dayStyles={item.styles} month={month} year={year} boxWidth={boxWidth} isWithinRange={item.isWithinRange} dayNumber={item.dayNumber} startEndStyles={item.startEndStyles} handleDateSelect={handleDateSelect} /> ))}
额外优化建议
- 避免在渲染循环中调用
format函数,尽量在预计算阶段完成所有日期格式化,减少重复计算。 - 对于固定的月份名称映射,可以提取到组件外部,避免每次渲染都创建新对象。
内容的提问来源于stack exchange,提问作者ProEvilz
相关产品推荐
相关产品推荐

