RN 列表里的「局部状态」和「全局状态」边界

@[toc]

如果你做过一段时间 RN,一定遇到过这种场景:

  • 点赞一个列表 item,整个列表都在重渲染
  • 改了一个筛选条件,滑动开始卡
  • 列表一多,哪怕只改一个布尔值,Profiler 里一片红

最后大家往往会得出一个结论:

FlatList 性能不行。

但只要你稍微深入看一眼,就会发现一个事实:

FlatList 只是"暴露问题"的地方,真正的问题是状态边界划错了。

这一篇,我们就只做一件事:
把 RN 列表里的"局部状态"和"全局状态"彻底掰开讲清楚。

不讲虚的,不上来就丢 Redux、Zustand、Recoil,

先回到最原始的问题:

👉 这个状态,到底属于谁?

一、一个最常见、也最容易踩坑的列表写法

我们先从一个"看起来完全没问题"的 Demo 开始。

场景

  • 一个 Feed 列表
  • 每个 item 可以点赞
  • 点赞后数字 +1,红心高亮

代码(问题版)

tsx 复制代码
function FeedPage() {
  const [list, setList] = useState(
    Array.from({ length: 100 }, (_, i) => ({
      id: i,
      liked: false,
      count: 0,
    }))
  );

  const onLike = (id: number) => {
    setList(prev =>
      prev.map(item =>
        item.id === id
          ? { ...item, liked: !item.liked, count: item.count + 1 }
          : item
      )
    );
  };

  return (
    <FlatList
      data={list}
      keyExtractor={item => item.id.toString()}
      renderItem={({ item }) => (
        <FeedItem item={item} onLike={onLike} />
      )}
    />
  );
}
tsx 复制代码
function FeedItem({ item, onLike }) {
  console.log('render item', item.id);

  return (
    <TouchableOpacity onPress={() => onLike(item.id)}>
      <Text>{item.liked ? '❤️' : '🤍'} {item.count}</Text>
    </TouchableOpacity>
  );
}

你会看到什么?

  • 点赞一个 item
  • 100 个 item 全部打印 render
  • 滑动明显变卡

很多人到这一步就开始:

  • memo
  • useCallback
  • extraData
  • shouldComponentUpdate

但如果你停下来问一句:

"点赞"这个状态,本质上是谁的状态?

你就会意识到,这里一开始就把边界画错了

二、RN 的核心问题:状态更新是"向下扩散"的

在 RN(准确说是 React)里,有一个非常重要但经常被忽略的事实:

状态一旦放在父组件,它的更新一定会导致子树重新计算。

哪怕你写了:

  • React.memo
  • useCallback
  • PureComponent

它们只能"减轻后果",不能改变扩散方向。

在上面的代码里:

  • listFeedPage

  • 每一次点赞:

    • setList
    • FeedPage 重新 render
    • FlatList 的 renderItem 重新执行
    • 所有 FeedItem 都被重新计算

👉 这就是典型的"状态上提过度"

三、为什么这个问题在 RN 列表里特别容易被放大?

这里要引入一个很关键的对比视角:页面模型差异

Web(Vue / DOM)的直觉

在 Web 里,很多人会下意识觉得:

  • 改一个 DOM
  • 只影响那一块

因为:

  • DOM 是节点级别更新
  • 模板是"结构 + 指令"
  • 状态和视图之间有"中间层"

RN(React)的真实模型

RN 的真实模型是:

  • 状态 → 函数组件重新执行 → 生成新的 UI 描述
  • 列表不是"节点集合"
  • 而是"函数 render 的多次调用"

所以在 RN 里:

你把状态放在哪一层,本质上决定了重渲染的扩散半径。

四、重新定义什么是「局部状态」

我们先给一个非常实用的判断标准:

如果一个状态,只影响一个列表 item 的展示,它就不该出现在列表的父组件里。

在点赞这个例子里:

  • liked
  • count

本质上是 item 自己的状态,而不是列表的状态。

五、第一种拆法:把状态下沉到 Item 内部

我们直接改写刚才的 Demo。

改造后的代码

tsx 复制代码
function FeedPage() {
  const data = useMemo(
    () => Array.from({ length: 100 }, (_, i) => ({ id: i })),
    []
  );

  return (
    <FlatList
      data={data}
      keyExtractor={item => item.id.toString()}
      renderItem={({ item }) => <FeedItem id={item.id} />}
    />
  );
}
tsx 复制代码
function FeedItem({ id }: { id: number }) {
  const [liked, setLiked] = useState(false);
  const [count, setCount] = useState(0);

  console.log('render item', id);

  const onLike = () => {
    setLiked(v => !v);
    setCount(c => c + 1);
  };

  return (
    <TouchableOpacity onPress={onLike}>
      <Text>{liked ? '❤️' : '🤍'} {count}</Text>
    </TouchableOpacity>
  );
}

发生了什么变化?

  • 点赞一个 item
  • 只有这个 item 会 render
  • FlatList 完全不动
  • 滑动流畅度立刻改善

这一步,本质上是:

缩小状态的影响范围。

六、但问题来了:那"全局状态"放哪?

你可能马上会问:

那如果点赞结果要同步给服务端?

或者要在详情页展示?

或者退出再进要恢复?

这就引出了第二个问题:
什么才算"全局状态"?

七、一个非常容易用错的判断标准

很多人会这样判断:

  • 会请求接口的 → 全局状态
  • 多个页面用到的 → 全局状态

这在 RN 里非常危险

更合理的判断方式是:

这个状态的"读扩散范围"有多大?

举个对比例子

状态 是否全局
列表 item 是否展开
当前 Tab
用户登录态
点赞是否成功 否(通常)
点赞结果是否已同步

👉 行为是局部的,结果才可能是全局的。

八、正确的分层方式:行为局部,结果上浮

一个非常推荐的结构是:

复制代码
FeedPage
 ├─ FlatList
 │   ├─ FeedItem(局部 UI 状态)
 │   ├─ FeedItem
 │   └─ FeedItem
 └─ likeStore / service(结果状态)

示例:局部状态 + 全局同步

tsx 复制代码
function FeedItem({ id }) {
  const [liked, setLiked] = useState(false);

  const onLike = () => {
    setLiked(true); // 立即反馈
    likeService.sync(id); // 异步上报
  };

  return (
    <TouchableOpacity onPress={onLike}>
      <Text>{liked ? '❤️' : '🤍'}</Text>
    </TouchableOpacity>
  );
}

这里有一个非常重要的认知转变:

不是所有状态都需要"可追溯、可复原、可全局订阅"。

九、为什么 RN 项目更容易"状态污染"?

现在我们可以回到你前面写的那篇文章主题了:

为什么 RN 项目更容易暴露状态污染?

因为 RN 的状态:

  • 天然是函数级的
  • 更新会向下扩散
  • 列表又是"高密度组件树"

一旦边界画错,问题会被无限放大。

十、给跨端开发者的一个统一心智模型

最后总结成一句话,非常适合放在文章结尾:

在 RN 里,列表性能问题本质上不是渲染问题,而是"状态所有权"问题。

一个可复用的判断口诀

  • UI 行为 → 尽量局部
  • 跨页面语义 → 再考虑全局
  • 不要为了"好管理",提前牺牲渲染边界
相关推荐
foo1st2 小时前
HTML中常用HASH算法使用笔记
javascript·html·哈希算法
3824278272 小时前
python3网络爬虫开发实战 第二版:绑定回调
开发语言·数据库·python
星月心城2 小时前
面试八股文-JavaScript(第五天)
开发语言·javascript·ecmascript
wjs20242 小时前
PostgreSQL 时间/日期处理指南
开发语言
wniuniu_2 小时前
ceph心跳机制
开发语言·ceph·php
东方-教育技术博主2 小时前
IDEA 配置electron开发环境
前端·javascript·electron
阿里嘎多学长2 小时前
2025-12-25 GitHub 热点项目精选
开发语言·程序员·github·代码托管
Oxye2 小时前
服务器内存不足导致程序没完全起起来,报错Required type must not be null
java·开发语言
乾元2 小时前
自动化补丁评估与策略回滚:网络设备固件 / 配置的风险管理
运维·开发语言·网络·人工智能·架构·自动化