React Native开发:网络编程、高级主题与性能优化
1. 离线模式案例研究
在应用开发中,处理设备离线情况是非常重要的。我们可以使用
Net Info
作为触发器来重试失败的请求,同时也可以在设备离线时管理用户的预期。基本做法是在屏幕顶部显示一个条,这样当某些功能无法正常工作时,用户就不会感到太惊讶。
以下是实现离线指示器的代码示例:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
offline: false // 1) 添加新状态来标记网络状态
};
}
componentDidMount() {
NetInfo.addEventListener(state => { // 2) 使用Net Info监听网络状态,并相应地切换之前创建的状态
if (!state.isConnected) {
this.setState({offline: true});
} else {
this.setState({offline: false});
}
});
}
render() {
return (
<SafeAreaView style={{width: '100%', height: '100%'}}>
<Moment/>
{this.state.offline && // 3) 失去连接时显示离线标签
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: 60,
backgroundColor: '#4ea4eb',
justifyContent: 'center',
alignItems: 'center',
paddingTop: 20
}}
>
<Text style={{
fontSize: 16,
fontWeight: "bold",
color: 'white'
}}>No Network</Text>
</View>
}
</SafeAreaView>
);
}
}
export default App;
上述代码的步骤如下:
1. 添加一个新状态来标记网络状态。
2. 使用
Net Info
监听网络状态,并相应地切换之前创建的状态。
3. 失去连接时显示离线标签。
2. 渲染回顾
React使用虚拟DOM树(VDOM树)来促进渲染。虚拟DOM树是内存中对正在渲染的组件的表示,每个VDOM都是一个组件。
render()
方法返回的是一个蓝图(元素),React运行时会使用它来构建VDOM。
render()
的级联调用最终提供了用于完成应用程序VDOM树的元素片段。
当进行更新时,React会在VDOM树上使用差异算法(协调)来收集更改,然后再进行实际的重新渲染。差异算法会逐层比较虚拟DOM树:
- 如果旧版本和新版本的根节点类型不同,则卸载并重新挂载子树。
- 如果相同,则使用新的props更新节点。
需要注意的是,
setState()
操作比较耗时,不恰当的使用可能会触发整个VDOM树的过度重新渲染。
为了提高差异算法的效率,React主要提供了三种方法:
-
shouldComponentUpdate()
:让你有机会决定在渲染事件中组件及其根子树是否应该更新。使用此方法可以让组件只监听某些props,从而有效减少差异算法中涉及的节点数量。但需要注意的是,它可能会无意中阻止对子组件的有效更新。
- 纯组件(
PureComponent
):为了使更新流程更加明确和可预测,一般做法是浅比较传入的所有props。这种做法已经被推广到一个标准组件
PureComponent
中。
- Redux:后续会详细介绍。
当列表使用
map()
或
FlatList
渲染时,差异算法在列表发生变化时可能会导致不必要的重新渲染。为了解决这个问题,建议为列表中的每个组件附加键。需要注意的是,使用列表索引作为键是一种反模式,应该始终为列表项使用显式键。
3. Redux
Redux是一个常用的全局状态管理框架。状态本质上是视图模型,通常应该是局部的。全局视图模型似乎违反直觉且过度设计,但它解决了跨VDOM树通信的基本难题,即当一个事件来自树中的一个组件,而更新应该在另一个组件上进行时。
以一个典型的视频播放器组件为例,播放/暂停按钮组件上的点击事件应该改变屏幕组件的状态,而屏幕组件可能位于完全不同的子树中。使用传统的React本地状态管理,我们需要将一些回调从屏幕组件一直传递到播放按钮,当回调较多时,会在VDOM树的上下文中出现回调地狱问题。
Redux提供了一种直接的方式,将组件中发生的事件传达给VDOM树上其他正在监听该事件的任意组件。事件的成功传递可能会导致目标组件的状态改变和重新渲染。
除了上述明显的好处外,Redux还带来了三个主要的附带优势:
|优势|描述|
|----|----|
|差异算法效率|使用Redux,更改可以直接发送到特定组件,因此只需要对该子树进行差异比较。|
|单向数据变更流|这使得状态变更更加合理和可追溯,特别是在一个状态片段(如点赞数)可能由多个来源(从服务器的网络获取和用户交互)更改的情况下。|
|采用动作语义|它采用了一种称为“动作”的语义,将无意义、有时相互依赖的单个属性的设置器聚合为有意义、更易于管理的逻辑单元。|
Redux引入了一些基本概念:
- 存储(
store
):全局状态在一个与reducer关联的存储中进行管理。
- 减速器(
reducer
):用于修改存储中的状态。
- 调度器(
dispatcher
):用于将动作发送到reducer。
- 动作(
action
):包含执行更改所需的所有信息(有效负载)。
- 订阅者(
subscriber
):监听状态的变化。
Redux遵循不可变性原则,即不直接修改全局状态的内容,而是始终创建更改状态的新实例,并将修改后的状态版本设置回存储中。
以下是一个使用Redux实现点赞功能的案例:
3.1 Reduxfy Feeds
首先,我们需要创建存储和reducer:
import { createStore, combineReducers } from 'redux';
const INITIAL_STATE = {
feeds: [], // 1) 定义存储中的根对象
};
const feedsReducer = (state = INITIAL_STATE, action) => {
let newState = state;
switch (action.type) { // 2) 一个reducer可以包含多个动作
case 'UPDATE_FEEDS':
if (!action?.payload?.feeds) {
console.error(
'action?.payload?.feeds is null in [UPDATE_FEEDS]'
);
return state;
}
newState = { // 3) 在Redux范式中,应始终为存储对象创建新实例
...state,
feeds: action?.payload?.feeds
};
break;
case 'LIKE':
// 后续实现点赞逻辑
default:
break;
}
return newState;
};
export default createStore(combineReducers({
moment: feedsReducer // 1) 在存储中定义reducer及其根对象
}));
步骤如下:
1. 定义reducer(
feedsReducer
)及其在存储中的根对象(
moment
)。
2. 一个reducer可以包含多个动作,每个动作以自己的方式修改存储中的对象。
3. 在Redux范式中,应始终为存储对象创建新实例,而不是直接修改它。
然后,我们定义更新存储的动作,并将动作与调度以props的形式连接起来:
const mapDispatchToProps = dispatch => (
bindActionCreators({
updateFeeds: (feeds) => { // 1) 使用内联闭包实现动作
return {
type: 'UPDATE_FEEDS',
payload: { feeds }
}
},
}, dispatch)
);
需要注意的是,使用动作时不要产生副作用,动作应该是纯函数。
接下来,在
loadData()
方法中使用调度:
async loadData() {
// ...
// --this.setState({data: feedsModel});
++this.props.updateFeeds(feedsModel);
// ...
}
最后,将调度和存储与
Moment
的props连接起来,使其成为订阅者:
const mapStateToProps = (state) => {
const { moment } = state
return { feeds: moment?.feeds }
};
const mapDispatchToProps = dispatch => (
// ...
);
export default withErrorBoundary(
connect(mapStateToProps, mapDispatchToProps)
(Moment), ErrorPage, undefined
);
3.2 实现点赞功能
以下是点赞动作在reducer中的逻辑分支:
const feedsReducer = (state = INITIAL_STATE, action) => {
let newState = state;
switch (action.type) {
case 'UPDATE_FEEDS':
// ...
case 'LIKE':
if (action?.payload?.feedIndex === undefined ||
action?.payload?.feedIndex === null ||
action?.payload?.feedIndex < 0 ||
action?.payload?.feedIndex >= state.feeds.length
) {
console.error('action?.payload?.feedIndex is not valid in [LIKE]:' + action?.payload?.feedIndex);
return state;
}
newState = { // 1) 遵循Redux的不可变性原则,为包含更改的每个层创建新实例
...state,
feeds: state.feeds.map((feed, index) => {
if (index === action.payload.feedIndex) {
feed.meta.numOfLikes += 1; // 2) 修改相关字段以供UI使用
feed.meta.liked = true;
feed.meta = Object.assign({}, feed.meta);
return Object.assign({}, feed);
} else {
return feed;
}
})
};
default:
break;
}
return newState;
};
步骤如下:
1. 遵循Redux的不可变性原则,为包含更改的每个层创建新实例。如果没有这一步,渲染将根本不会触发。
2. 修改相关字段以供UI使用。
接下来,我们将调度连接到包含点赞按钮的高阶组件
withMetaAndControls
:
export default function withMetaAndControls(Feed) {
class ElemComponent extends React.Component {
render() {
return (
<View style={[
{...this.props.style}, styles.commonPadding
]}>
<View style={styles.metaContainer}>
<LoomingImage
style={styles.avatar}
source={{uri: this.props.item.meta.avatarUri}}
/>
<View style={styles.infoContainer}>
<Text style={styles.userName}>
{this.props.item.meta.name}
</Text>
<Text style={styles.date}>
{this.props.item.meta.date}
</Text>
</View>
</View>
<Feed {...this.props} ref={this.props.innerRef}/>
<View style={styles.controlContainer}>
<TouchableOpacity // 3) 用TouchableOpacity包裹点赞按钮,并将新创建的调度附加到它
disabled={this.props.item.meta.liked} // 5) 点赞时禁用按钮
style={{flex: 1}}
onPress={
this.props.like.bind(this, this.props.feedIndex)
}
>
<NumberedWidget
type={
this.props.item.meta.liked ? // 4) 根据点赞状态更改按钮样式
widgetTypes.LIKED :
widgetTypes.LIKE
}
number={this.props.item.meta.numOfLikes}
/>
</TouchableOpacity>
<NumberedWidget
style={{flex: 1}}
type={widgetTypes.COMMENT}
number={this.props.item.meta.numOfComments}
/>
<NumberedWidget
style={{flex: 1.5}}
type={widgetTypes.SHARE}
number={this.props.item.meta.numOfShares}
/>
<Widget type={widgetTypes.MORE} />
</View>
</View>
)
}
}
const mapDispatchToProps = dispatch => ( // 1) 像之前一样使用内联动作建立调度
bindActionCreators({
like: (feedIndex) => {
return {
type: 'LIKE',
payload: { feedIndex }
}
},
}, dispatch)
);
const ConnectedElemComponent = connect( // 2) 将调度与高阶组件ElemComponent连接
null, mapDispatchToProps)(ElemComponent
);
return React.forwardRef((props, ref) =>
<ConnectedElemComponent
innerRef={ref} {...props}
/>
);
}
步骤如下:
1. 像之前一样使用内联动作建立调度。
2. 将调度与高阶组件
ElemComponent
连接。
3. 用
TouchableOpacity
包裹点赞按钮,并将新创建的调度附加到它。执行reducer后,步骤4和5的更改以及点赞数将自然发生。
4. 根据点赞状态更改按钮样式。
5. 点赞时禁用按钮。
4. 长列表处理
在React Native中,长列表不再是一个问题。官方提供的长列表组件是
FlatList
,它相当于iOS的
TableView
和Android的
recyclerview
。基本思路是始终渲染一个比视口大但比整个列表小得多的区域,这样在滚动时可以以最小的内存占用给人一种整个列表都已完全渲染的错觉。
在大多数情况下,使用默认配置就足够了。如果需要进一步优化,可以对
FlatList
的一些参数进行微调。以下是一些可以直接应用的基本优化点:
- 如前面提到的,为列表项添加键可以减少差异算法的开销。
- 使用
shouldComponentUpdate
或纯组件来避免整个列表的重新渲染。
如果
FlatList
最终无法满足性能要求,还可以使用第三方库如
RecyclerListView
来进一步提高性能潜力。
React Native开发:网络编程、高级主题与性能优化
5. 长列表性能优化总结
为了更清晰地展示长列表性能优化的要点,我们可以用一个mermaid流程图来概括:
graph LR
A[长列表性能优化] --> B[基本优化]
A --> C[高级优化(可选)]
B --> B1[添加键到列表项]
B --> B2[使用shouldComponentUpdate或纯组件]
C --> C1[微调FlatList参数]
C --> C2[使用第三方库(如RecyclerListView)]
通过这个流程图,我们可以直观地看到长列表性能优化的不同层次和方法。基本优化点是可以直接应用且效果明显的,而高级优化则是在基本优化无法满足需求时的进一步选择。
6. 不同优化方法对比
我们可以将前面提到的各种优化方法进行对比,以便更好地理解它们的适用场景和优缺点。
| 优化方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 添加键到列表项 |
列表渲染,尤其是使用
map()
或
FlatList
时
| 减少差异算法的开销,提高渲染性能 | 无明显缺点,但需注意避免使用列表索引作为键 |
使用
shouldComponentUpdate
| 组件更新频繁,需要控制渲染范围 | 有效减少差异算法中涉及的节点数量,避免不必要的重新渲染 | 可能会无意中阻止对子组件的有效更新 |
纯组件(
PureComponent
)
| 组件更新逻辑相对简单,可通过浅比较props来决定是否更新 | 更新流程明确,代码简洁 | 不适合复杂的更新逻辑,因为浅比较可能无法满足需求 |
| Redux | 跨组件状态管理,尤其是涉及多个组件之间的通信和状态更新 | 解决跨VDOM树通信难题,提高差异算法效率,实现单向数据变更流 | 学习成本较高,代码复杂度增加 |
微调
FlatList
参数
| 长列表渲染性能仍需进一步提升 | 根据具体情况调整参数,可实现更好的性能 |
需要对
FlatList
的参数有深入了解,调整不当可能会影响性能
|
使用第三方库(如
RecyclerListView
)
|
FlatList
无法满足性能要求
| 进一步提高性能潜力 | 引入额外的依赖,增加了项目的维护成本 |
7. 实际应用中的注意事项
在实际的React Native开发中,我们需要根据具体的项目需求和场景来选择合适的优化方法。以下是一些实际应用中的注意事项:
7.1 状态管理
- 合理划分全局状态和局部状态:虽然Redux可以实现全局状态管理,但并不是所有状态都需要放在Redux中。对于与单个组件相关的UI和动画状态,使用局部状态更为合适。只有那些被多个实体更改或监听的状态,才适合使用Redux进行管理。
- 避免滥用Redux:不要直接将业务模型或服务响应复制为Redux存储,这可能会导致渲染时的复杂计算。应该将复杂的逻辑推到解析和反序列化过程或专门的适配器层。同时,避免定义单个reducer和对象用于整个存储,应根据不同的关注点分散reducer及其关联对象。
7.2 列表渲染
- 谨慎使用列表索引作为键:如前面所述,使用列表索引作为键是一种反模式,可能会导致列表变更时键的变化,从而引发不必要的重新渲染。始终为列表项使用显式键。
-
结合多种优化方法:在处理长列表时,可以结合使用添加键、
shouldComponentUpdate或纯组件等优化方法,以达到更好的性能效果。
7.3 性能测试
-
定期进行性能测试:在开发过程中,定期对应用进行性能测试,确保各项优化措施有效。可以使用React Native提供的性能监测工具,如
InteractionManager、Performance等,来分析应用的性能瓶颈。 -
根据测试结果调整优化策略:如果性能测试结果不理想,应根据具体情况调整优化策略。例如,如果发现
FlatList的性能仍无法满足要求,可以考虑使用第三方库进行进一步优化。
8. 总结
在React Native开发中,网络编程、渲染优化、状态管理和长列表处理是非常重要的方面。通过合理运用离线模式处理、Redux全局状态管理、渲染优化方法和长列表性能优化技巧,我们可以开发出性能优良、用户体验良好的应用程序。
在实际开发中,我们需要根据具体的项目需求和场景,灵活选择合适的技术和方法,并不断进行性能测试和优化。希望本文介绍的内容能够对React Native开发者有所帮助,让大家在开发过程中少走弯路,提高开发效率和应用质量。
超级会员免费看
1015

被折叠的 条评论
为什么被折叠?



