React Native数组变更时为何重渲染所有组件?
我正在开发一款聊天应用,在React Native中使用FlatList组件或map()方法渲染消息列表时,只要消息数组内容变更,所有列表项都会被重渲染;但在React JS中采用相同逻辑实现却不会出现这个问题,请问这是什么原因?
UPDATE
React JS 正常实现代码
const Chat = ({ roomid }) => { const messages = useSelector(state => state.chat.messages[roomid]) ****more vars*** useEffect(() => { if (refChat) refChat.current.scrollTop = refChat.current.scrollHeight }, [messages]) return ( <div id="chat" className="h-full c-grid-sm md:c-grid-md dark:bg-container bg-slate-800"> <div className="h-full flex flex-col w-full bg-white dark:bg-container"> <ScrollableDivVertical ref={refChat}> {messages && messages.map((m, i) => <Message key={i} canColors={canColors} fontSize={fontSize} scribbleSize={scribbleSize} user={m.user} customname={m.type === "publicmsg" && areswebsupport ?canColors ? users[m.userid]?.parsedcustomnamefc : users[m.userid]?.parsedcustomname:undefined} text={m.text} type={m.type} areswebsupport={areswebsupport} />)} </ScrollableDivVertical> <ChatInputText roomid={roomid} type={1} /> </div> </div> ) } export default memo(Chat)
React Native 存在重渲染问题的代码
const MessagesList = ({ navigate }) => { const messages = useSelector(state => state.chat.messages[roomid]) ***other vars**** const refMessagesList = useRef(null) return ( <StyledView style={{ flex: 9 }} bg={isDark ? colors.gray : colors.light} fd="column" onTouchEnd={() => Keyboard.dismiss()}> <StyledView bg={isDark ? colors.dark : colors.blue} pb="5px" pt="5px" jc="center" ai="center" > <IconButton h='30px' w="10%" bg="transparent" icon="leftcircleo" onPress={() => navigate("OpenChats")} sizeIcon={30} colorIcon={colors.swhite} /> <StyledView bg="transparent" w="90%"> <StyledText fs="20px" ta="center" w="100%" fg={colors.swhite}>{roomname}</StyledText> </StyledView> </StyledView> <ScrollView ref={refMessagesList} onContentSizeChange={() => refMessagesList.current.scrollToEnd({ animated: false })} style={{ backgroundColor: isDark ? colors.gray : colors.light, flex: 1, width: "100%" }} scrollsToTop={true}> {messages && messages.map((message, index) => ( <MessageItem key={message.type ? message.type + index : "default" + index} k={message.type ? message.type + index : "default" + index} item={{ ...message, isDark, fontSize, scribbleSize, width, canColors }} customname={message.type === "publicmsg" && areswebsupport ? canColors ? users[message.userid]?.parsedcustomnamefc : users[message.userid]?.parsedcustomname : undefined} areswebsupport={areswebsupport} ></MessageItem> ))} </ScrollView> </StyledView> ) }
核心原因拆解
组件未做memo优化:React JS的
Chat组件用了memo导出,能避免无意义的重渲染;但React Native的MessagesList和MessageItem都没加memo,父组件一更新,所有子组件跟着重渲染。引用类型props频繁变更:
React Native里给MessageItem传的item是每次渲染都新创建的对象{ ...message, ... },哪怕内容没变,引用地址也会变,就算MessageItem加了memo,浅比较也会认为props变了,触发重渲染。而React JS的Message组件传的都是基础类型或直接取的消息属性,没有新创建引用类型。滚动容器的优化差异:React Native的ScrollView没有FlatList的虚拟渲染和重渲染拦截逻辑,用map遍历渲染所有消息项,本身就比FlatList开销大;而React JS的滚动容器是原生div,浏览器会做部分渲染优化,但这不是核心原因。
key的稳定性不足:两者的key都依赖index,虽然聊天消息追加场景下index问题不大,但如果用消息的唯一ID(比如后端返回的msgId)当key,能进一步减少不必要的组件卸载/重建,不过这不是全量重渲染的主因。
解决办法
给组件加memo包装:
给MessagesList和MessageItem都用React.memo包裹,比如:export default React.memo(MessagesList) // MessageItem组件同理 export default React.memo(MessageItem)优化props传递方式:
不要每次创建新的item对象,把isDark、fontSize等基础类型单独作为props传递,或者用useMemo缓存合并后的对象:// 方式1:拆分props <MessageItem key={message.id} // 用消息唯一ID当key message={message} isDark={isDark} fontSize={fontSize} scribbleSize={scribbleSize} width={width} canColors={canColors} // ...其他props /> // 方式2:用useMemo缓存 const memoizedItem = useMemo(() => ({ ...message, isDark, fontSize, scribbleSize, width, canColors }), [message, isDark, fontSize, scribbleSize, width, canColors]) <MessageItem key={message.id} item={memoizedItem} />改用FlatList并配置优化项:
替换ScrollView为FlatList,设置keyExtractor为消息唯一ID,同时确保renderItem里的组件是memoized的:<FlatList data={messages} keyExtractor={(item) => item.id} // 用唯一ID renderItem={({ item }) => ( <MessageItem message={item} isDark={isDark} /> )} // 自动滚动到底部 onContentSizeChange={() => refMessagesList.current.scrollToEnd({ animated: false })} ref={refMessagesList} />检查Redux状态的不可变性:确保每次新消息加入时,
state.chat.messages[roomid]是返回新数组(比如用[...oldMessages, newMessage]),而不是修改原数组,这是Redux的基本要求,也能保证useSelector返回的引用正确触发更新。
内容的提问来源于stack exchange,提问作者soyunewok

