3.1 JSX与组件模型


3.1 JSX与组件模型

本节摘要:JSX 不是模板语言,而是「界面描述」的语法外衣;组件不是画好的图,而是一台接收属性、维护状态、随时重新描述界面的机器。理解「描述与执行分离」这条主线,你就能解释为什么同一份组件代码能在 iOS 与 Android 上各自落地,也能在动手写组件之前先想清楚属性与状态的边界。本节是本章的单元级地基,3.2 的差异管理、3.3 的布局、3.4 的交互都建立在它之上。

先记住一条渲染公式

承接第 2 章的架构视角,现在把镜头从引擎舱移到驾驶座。React 世界只有一条核心公式:界面是状态的函数。输入什么状态,输出什么界面描述;状态变了,函数重新执行,产出新的描述。RN 的特别之处只在这条公式的下游——描述不交给浏览器,而是交给原生渲染器去两片土壤上执行。于是组件的全部职责可以概括为三件事:接收外部传入的属性,维护自己内部的状态,然后把两者合成为界面描述。

这三件事划出了两条边界。属性与状态的边界:「从外部来、自己不改」的是属性,「自己拥有、自己变更」的是状态——把本该由外部传入的东西写成内部状态,组件就失去了被复用与被测试的可能。描述与执行的边界:你在 JSX 里写的不是绘图指令,而是一份声明式提货单,RN 拿着提货单去两端的原生仓库取货。下面这张图把整条链路画了出来。

图:从 JSX 描述到双端原生视图的映射链路

图:从 JSX 描述到双端原生视图的映射链路

动手写一个完整的受控组件

概念说完,用一个「订单备注卡片」组件把三件事串起来。它接收外部传入的订单号与初始备注(属性),内部维护「是否展开编辑」的状态,并在属性变化时同步内部状态——这是受控组件的典型形态:

import React from 'react'; import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native'; export function OrderNoteCard({ orderId, initialNote = '', onNoteChange }) { // 内部状态:只描述「卡片自己知道的事」——编辑区是否展开 const [editing, setEditing] = React.useState(false); // 草稿与已提交值分离,避免每敲一个字就触发回调 const [draft, setDraft] = React.useState(initialNote); const submit = () => { setEditing(false); // 对外通知交给回调属性,卡片不关心上游怎么用 onNoteChange?.(orderId, draft); }; return ( <View style={styles.card}> <View style={styles.header}> <Text style={styles.orderId}>订单 {orderId}</Text> <Pressable onPress={() => setEditing(v => !v)}> <Text style={styles.toggle}>{editing ? '收起' : '编辑备注'}</Text> </Pressable> </View> {editing ? ( <View> <TextInput style={styles.input} value={draft} multiline maxLength={200} onChangeText={setDraft} placeholder="填写备注,200 字以内" /> <Pressable style={styles.submitBtn} onPress={submit}> <Text style={styles.submitText}>保存</Text> </Pressable> </View> ) : ( <Text style={styles.note}>{initialNote || '暂无备注'}</Text> )} </View> ); } const styles = StyleSheet.create({ card: { padding: 14, borderRadius: 10, backgroundColor: '#fff', borderWidth: StyleSheet.hairlineWidth, borderColor: '#d9e2ec' }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, orderId: { fontWeight: '600', color: '#2c3e50' }, toggle: { color: '#1a73e8' }, input: { borderWidth: StyleSheet.hairlineWidth, borderColor: '#c3cfda', borderRadius: 8, padding: 8, minHeight: 64, textAlignVertical: 'top', marginTop: 10 }, note: { color: '#5f7182', marginTop: 10 }, submitBtn: { alignSelf: 'flex-end', marginTop: 8, paddingHorizontal: 14, paddingVertical: 6, borderRadius: 8, backgroundColor: '#1a73e8' }, submitText: { color: '#fff' }, });

三个设计决定值得咀嚼。其一,草稿与已提交值分离:输入过程只更新草稿,保存那一刻才回调上游——否则每敲一个字符都向上传播,上游一旦把值又传回来,就会形成受控循环的抖动。其二,对外通知只走回调属性:卡片不假设上游是谁,测试时传一个记录函数就能验证行为。其三,样式全部收敛到底部的 StyleSheet,正文中只留结构——这是 3.3 节要展开的组织纪律。

完整案例:一个写坏属性边界的组件

背景:某团队的地址选择卡片最初把「选中地址」写成内部状态,业务方随后提出需求:从订单页返回时,地址选择要恢复为上次离开时的选中项。操作:由于选中态藏在卡片内部,上游根本无法注入,团队只能给卡片加了三个带副作用的实例方法供外部调用;接着测试发现初始化时序不稳定,又补了事件通知机制;两个月内这张卡片积攒了五个专供外部的「后门」。结果:重构时把选中地址提升为受控属性,卡片内部只保留「点击行为」这一项私有状态,五个后门删掉四个。解读:状态放错位置,最终一定会以「外部想要却又拿不到」的形式返工——判断口诀是:如果某个值需要被上游读取或设置,它就该是属性;只有纯粹「组件私有的操作感」才配当状态。变式:并非一切都要受控,输入框的焦点态、按压高亮这类瞬时视觉状态留在内部反而干净——边界感来自对「谁需要知道」的持续追问,而不是一刀切的教条。

最后提示两个新手高频错误,都源于对渲染公式的误解。错误一:在渲染函数体内直接调用状态更新函数且不带条件,状态一变、重新渲染、再变,无限循环直到报错——更新逻辑要放进事件回调或副作用钩子里。错误二:把接口返回的数据既存进状态又用属性传来传去,两份真相互不同步——同一份数据只允许一个所有者,要么全归上游,要么全归自己。

本节要点回顾

  • 界面是状态的函数:组件接收属性、维护状态、输出描述,三件事之外都是多余;
  • 属性与状态的边界口诀:上游需要读写的是属性,组件私有的瞬时感是状态;
  • JSX 是提货单不是图纸:描述与执行分离,同一份描述在两座原生仓库各自取货;
  • 草稿与提交值分离:输入类组件避免逐字符向上传播,防止受控循环抖动;
  • 渲染循环两大错误:渲染体内无条件更新状态、同一数据两个所有者。

单元模型就位,下一节盘点核心组件各自的脾气——尤其是那些 iOS 与 Android 表现不同的地方,以及Platform 分流的工程用法。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U