@[toc]
很多人会觉得:
RN 卡,是 RN 的问题
Web 顺,是浏览器牛
但当你同时踩过 RN 和 Web 两边的坑之后,反而会意识到一件事:
RN 不是"更差",而是更早、更赤裸地暴露了架构问题。
而 Vue 列表里,很多"看起来没出事"的写法,其实只是被浏览器兜住了而已。
一、为什么 RN 的问题,反而更有"教学价值"
先说一个结论:
RN 的渲染模型,比 Vue 更"诚实"。
在 RN 里:
- 每一次 state 更新
- 每一次 render
- 每一次 prop 变化
都会非常直接地体现在:
- 掉帧
- 滑动卡顿
- 动画不跟手
它不会帮你兜底,也不会"差不多就算了"。
而 Web:
- 浏览器 diff 更快
- DOM 操作被高度优化
- 滚动是原生的
于是很多问题被延迟暴露,甚至永远不暴露。
二、RN 列表里我们已经学到的三条铁律
在反推 Vue 之前,先回顾一下 RN 列表中已经被反复验证过的结论:
1. 列表性能问题,本质是"渲染扩散"
不是 FlatList 慢,而是:
- 一个状态变化
- 引发了不该更新的 item
2. 列表最怕"广播式状态"
- Context
- Redux
- 父组件 state
只要被列表整体订阅,都会放大问题。
3. Item 的"交互态"必须局部化
点赞、选中、展开,这些状态:
- 生命周期短
- 影响范围小
- 不应该穿透列表
三、把这三条,直接套回 Vue 列表
现在我们来看一个非常常见、甚至被当成"规范"的 Vue 写法。
四、一个看似合理,但暗藏风险的 Vue 列表示例
常见写法
vue
<template>
<div v-for="item in list" :key="item.id">
<Item
:item="item"
:selected="selectedId === item.id"
@select="handleSelect"
/>
</div>
</template>
<script setup>
const selectedId = ref(null)
function handleSelect(id) {
selectedId.value = id
}
</script>
在 Web 里:
- 点一个 item
- UI 正常
- 性能看起来没问题
但如果你用 RN 的视角 看这段代码,会立刻警觉。
五、用 RN 的视角"拆渲染链路"
当你点击某一项时:
selectedId更新- 父组件重新 render
- v-for 重新执行
- 所有 Item 的 props 重新计算
- 所有 Item 都进入一次 diff
只是因为浏览器扛得住,你才没感觉到。
但逻辑扩散是真实存在的。
六、RN 会怎么写这件事?
在 RN 里,这种写法几乎一定会被改。
RN 的第一反应是:
这个 selected 状态,真的需要放在列表外吗?
于是更合理的写法是:
jsx
const Item = React.memo(({ item }) => {
const [selected, setSelected] = useState(false)
return (
<Pressable onPress={() => setSelected(v => !v)}>
<Text>{selected ? '选中' : '未选中'}</Text>
</Pressable>
)
})
点击:
- 只更新当前 item
- 列表不动
- 渲染范围极小
七、反推 Vue:列表 item 的交互态,应该内聚
改造后的 Vue 写法
vue
<Item :item="item" />
vue
<script setup>
const props = defineProps<{ item: Item }>()
const selected = ref(false)
function toggle() {
selected.value = !selected.value
}
</script>
这时候:
- 父组件不再感知 selected
- 列表不再参与交互渲染
- diff 范围被限制在 item 内
八、Vue 里"看起来没问题"的 Context / Store,用 RN 一看全是坑
再看一个更隐蔽的例子。
Vue 中央 store 驱动列表 UI
js
const selectedId = computed(() => store.state.selectedId)
vue
<Item
v-for="item in list"
:key="item.id"
:active="item.id === selectedId"
/>
这在 RN 世界里等价于:
把列表交互态放进 Redux
我们已经知道结果了。
九、RN 教会我们的:UI 状态 ≠ 业务状态
这是跨端开发者最重要的一条分水岭。
业务状态适合:
- Vuex / Pinia
- Redux / Zustand
UI 交互态适合:
- Item 内部 state
- 组件私有响应式变量
如果你在 Vue 里区分不清这两类状态,很可能只是:
浏览器替你兜住了后果。
十、Vue 列表的"正确拆分模型",用 RN 语言总结
我们可以直接把 RN 的最佳实践翻译成 Vue 规则:
1. 列表父组件
只做三件事:
- 数据来源
- key 管理
- 布局结构
2. Item 组件
必须满足:
- 自己管理交互态
- 尽量少依赖外部响应式数据
- props 只接收"稳定数据"
3. 状态上浮的唯一理由
只有一种情况值得上浮:
多个 item 之间,真的存在强一致性需求。
比如:
- 只能选中一个
- 需要统一取消
- 跨 item 联动
即便如此,也要尽量:
- 用 id 而不是对象
- 用最小订阅范围
十一、一个"用 RN 反推 Vue"的完整对照 Demo
错误版(扩散模型)
vue
<script setup>
const activeId = ref(null)
</script>
<Item
v-for="item in list"
:active="item.id === activeId"
/>
正确版(局部模型)
vue
<Item v-for="item in list" :key="item.id" />
Item 内部:
vue
const active = ref(false)
十二、你会发现一个非常反直觉的结果
当你开始用 RN 的模型写 Vue:
- Vue 列表更好拆
- 组件边界更清晰
- 状态职责更干净
这不是因为 Vue 变强了,而是因为:
你不再依赖浏览器的"性能宽容度"了。