本文由葡萄城技术团队于博客园原创并首发
在上篇中, 我们介绍了什么是 Flexbox 布局, 以及如何使用 Flexbox 布局. 还没有看过的小伙伴欢迎回到文章列表点击查看之前的文章了解.
那么, 当我们有了基本的布局概念之后, 就可以做一些有意思的尝试了. 不过, 它们会有一个美中不足: 只是静静地呆在那里, 不接受反馈. 换句话说, 它们从应用开始到结束, 只有一种状态.
注意, 上面这句话其实包含了 RN 中 (当然同时也是 React 中) 两个非常重要的概念:
第一,"从应用开始到结束", 意味着它在时间上有一段生命周期(Life Cycle).
第二, 应用其实可以拥有很多种状态(State), 比如, 正常时是一种状态, 出错时是另一种状态. 而且这些状态能够在某些条件下进行转换.
基本概念:
在 RN 中, 界面的变化对应着程序状态的变化. 或者说, 界面的变化, 正是因为应用的状态发生了转换而导致的. 应用的状态主要由两个变量决定, props 和 state, 它们可以存在于继承自 React.Component 的每一个组件中. state 由组件自身定义, 用来管理组件及其子组件的状态. 而 props 来自于父组件, 在本组件中相当于常量, 它的改变方式只能来自于父组件.
在 RN 中, 界面的变化对应着程序状态的变化. 或者说, 界面的变化, 正是因为应用的状态发生了转换而导致的. 应用的状态主要由两个变量决定, props 和 state, 它们可以存在于继承自 React.Component 的每一个组件中. state 由组件自身定义, 用来管理组件及其子组件的状态. 而 props 来自于父组件, 在本组件中相当于常量, 它的改变方式只能来自于父组件.
state 和 props 都不允许手动地直接设值. 像 this.state.a = 1 或者 this.props.b = 2 这种代码是会报错的. 要改变 state, 只能是在本组件中调用 this.setState 方法. 而要改变 props, 只能依赖于它的值在传下来之前, 已经在其父组件中被改变.
既然在组件中, state 属性无论从字面含义还是程序语义上, 都是用来表示状态的, 那么为什么还需要一个 props 属性呢?
我的理解主要有两个原因.
第一, 因为有些组件其实是 "无状态" 的. 它们只是接受父组件传给它们的东西, 然后老老实实把它们渲染出来. 它们自己内部不保存任何状态, 它们只是对父组件状态的反应. 或者说:"它们不生产状态, 它们只是父组件状态的显示器." 父组件的状态通过 props 传递给子组件. 我们经常会构造这种无状态的组件, 因为它职责单一, 封装性好, 可作为更复杂组件的基石.
第二, 由于父组件与子组件之间往往需要联动, props 就是最直接的提供联动的手段. 父组件中构造子组件时, 就像函数调用的传参一样, 把需要的东西传给子组件的 props.
state 和 props 的重要特点是, 默认情况下. 当它们改变时, RN 会自动东西渲染与之相关的界面以保持和 state 与 props 同步. 为什么说 "默认情况下", 是因为我们可以利用生命周期函数手动 "截断" 这个渲染逻辑, 本文暂不涉及.
另外, 在 RN 中, 其实也可以使用不属于 props 和 state 的变量, 来手动控制组件的状态. 但是不推荐这么做. 因为这会使状态的控制方法变得不统一, 不利于后期维护.
开始尝试:
我们已经可以基于 state 与 props 的概念做一个小练习了. 它是一个 ToDo List, 也就是待办列表. 大概长下面这个样子:
To Do List 草图
我们把它分为两个页面. 最左边是添加待办事项的界面, 记为 ToDoListAdd. 中间和最右边其实是同一个界面, 记为 ToDoListMain, 它拥有两种不同状态.
我们先来看 ToDoListAdd 界面. 它有上中下三个部分. 最上面是一个可点击返回的头部, 中间是用于输入文字的 TextInput, 底部是一个确认添加的 Button.
有没有发现它和上一次我们的 flexbox 小练习界面很像呢? 没错, 基于上一篇的布局知识, 我们可以方便地把页面修改成这样.
再来看 ToDoListMain 界面, 它与 ToDoListAdd 很像. 头部的 "添加" 用以跳转至 ToDoListAdd."多选" 用以让每一个待办项的 Checkbox 显示出来, 并且显示最下面包含全选 Checkbox 的 footer.
要完整地完成这个应用, 本文的篇幅是不够的, 后续文章会深入到更加细节的地方. 但是首先, 本文会介绍如何实现以下基本功能: 1. 利用 state 控制编辑状态; 2. 利用 state 实现界面的跳转; 3. 父子组件之间的通信. 让我们着手编写程序, 穿插着介绍着三点内容.
步骤 1, 使用 flex 布局完成 ToDoListAdd 界面. 在根目录新建一个文件 ToDoListAdd.js, 定义 ToDoListAdd 类. 为更加简洁, 这里省去必要组件的引入代码, 以及样式代码.
- export default class ToDoListAdd extends Component<Props> {
- constructor(props) {
- super(props);
- }
- onPress() { } // 暂且为空
- render() {
- return (
- <View style={styles.container}>
- <View style={styles.header}>
- <Text style={styles.add} onPress={this.props.onBack}>返回</Text>
- </View>
- <View style={styles.body}>
- <TextInput />
- </View>
- <View style={styles.footer}>
- <Button title="确定" onPress={this.onPress} style={styles.btn} />
- </View>
- </View>
- );
- }
- }
值得注意的是, 这里 "返回" 按钮的 onPress 回调函数来自于 props. 也就是说, 当这个 ToDoListAdd 被使用的时候, 父组件需要指定传给子组件 (这里子组件就是 ToDoListAdd) 的 onBack 的值. 可以看到, 到目前为止, 上面的 ToDoListAdd 组件其实是一个 "无状态" 组件. 它只是对父组件传来的 props 的渲染. 但实际上, TextInput 通常是有状态的, 因为里面的值会随着用户的改动而变化. 这里我们暂时没有处理它.
步骤 2, 初步创建 ToDoListMain 组件. 当开始构思这个组件的时候, 至少有两件事情是需要考虑的:
待办事项的数据源, 应该来自那里? 显示和隐藏底部的状态存应该在哪里?
第一个问题, 为了叙述方便, 我们把待办事项的数据源用变量 todoList 来表示. todoList 可以理解为一些状态, 它是需要在 ToDoListMain 组件中被显示和操作的. 有两个 todoList 的可选位置, 要么放在 ToDoListMain 组件自身, 要么放在 ToDoListMain 更上一层的组件中. 于此同时, 当 ToDoListAdd 组件试图添加一个新的待办事项时, ToDoListAdd 组件是需要修改 todoList 这个数据源的. 如果 todoList 在 ToDoListMain 组件中, ToDoListAdd 组件就需要和 ToDoListMain 组件进行通信. 但这其实就绕了一个圈子, 因为从草图的逻辑上看, ToDoListAdd 是与 ToDoListMain 同级的一个界面, 它们之间要通信, 一般的做法是借助于共同的父组件. 所以, 就这个例子来说, 把数据源就直接放在 ToDoListAdd 和 ToDoListMain 共同的父组件中是更方便的选择. 接下来会看到, 这个共同的父组件就是 App.js, 它将引入 ToDoListAdd 和 ToDoListMain, 我们还会 App.js 中手动设置渲染选择的逻辑.
第二个问题, 显示和隐藏底部是只在 ToDoListMain 组件中使用的状态, 它不与外界有联系, 所以放在它内部即可.
经过上面的分析, 我们创建初步创建 ToDoListMain 如下:
- export default class ToDoListmain extends Component<Props> {
- constructor(props) {
- super(props);
- this.state = {
- isEditing: false
- };
- this.onEdit = this.onEdit.bind(this);
- this.renderItem = this.renderItem.bind(this);
- }
- renderFooter(toggleCheckAll, isAllChecked) {
- if (!this.state.isEditing) {
- return null;
- }
- const count = this.props.todoList.filter(item => item.isChecked).length;
- return (
- <View style={styles.footer}>
- <CheckBox onValueChange={toggleCheckAll} value={isAllChecked} />
- <Text style={styles.menu}>{` 已选择:${count}项 `}</Text>
- </View>
- );
- }
- onEdit() {
- this.setState((prevState) => {
- return {
- isEditing: !prevState.isEditing
- }
- });
- }
- renderItem(item) {
- return (<ToDoListItem {...item}
- toggleItemCheck={this.props.toggleItemCheck}
- isEditing={this.state.isEditing} />);
- }
- render() {
- const { toggleCheckAll, isAllChecked, onAddItem, todoList } = this.props;
- return (
- <View style={styles.container}>
- <View style={styles.header}>
- <Text style={styles.add} onPress={onAddItem}>添加</Text>
- <Text style={styles.title}>待办列表</Text>
- <Text style={styles.multi}
- onPress={this.onEdit}>{this.state.isEditing ? '取消' : '多选'}
- </Text>
- </View>
- <FlatList style={styles.body} isEditing={this.state.isEditing}
- data={todoList} renderItem={this.renderItem} />
- {this.renderFooter(toggleCheckAll, isAllChecked)}
- </View>
- );
- }
- }
我们看到该组件只有一个状态, isEditing. 它控制了左上角的文字是 "取消" 还是 "多选", 也控制了底部是否显示.
我们在控制底部是否显示时, 调用了一个自定义的函数, 用它的返回值最为内容插入在调用函数的位置. 在 RN 中, 如果在渲染的时候返回 null, 就表示什么也不渲染. 所以调用 renderFooter 时, 在 isEditing 状态为 false 时, 什么都不渲染.
toggleCheckAll 用来控制是否全选待办事项. isAllChecked 是判断是否全选. onAddItem 用作点击 "添加" 文字的回调. 而 todoList 就是最重要的待办事项的数据源了. 它们都来自 ToDoListMain 的父组件, 通过 props 传下来.
而 ToDoListMain 组件内部, 有一个 onEdit 函数, 用作右上角 "取消" 和 "多选" 文字 onPress 时的回调. 在里面我们看到 RN 中设置 state 的正确方式是调用 this.setState 方法.
另外, 为了演示方便, 这里使用官方提供的 Checkbox 组件来表示待办事项是否 check 了. 但这个 Checkbox 组件的其实只有 Android 平台才有, iOS 下没有. 而对 iOS 的处理, 打算在后面的文章中专门分享.
还有一点值得注意的地方, 是引入了 FlatList 组件来对 todoList 数据源进行渲染. FlatList 是官方提供的用意显示列表的组件, 老版本的 ListView 已经被标记为弃用了(deprecated).FlatList 组件对列表的渲染做了许多性能优化和功能增强. 我们暂时只是使用它来简单显示待办列表.
每一个待办事项使用了自定义的另一个组件 ToDoListItem, 我们马上来看看它.
步骤 3, 实现 ToDoListItem 组件. 它没有自己的状态, 也只是对父组件内容的展示.
- export default class ToDoListItem extends Component<Props> {
- constructor(props) {
- super(props);
- }
- render() {
- const { toggleItemCheck, item } = this.props;
- const { isChecked, detail, level } = item;
- const basicLevelStyle = styles.level;
- let specificLevelStyle;
- if (level === 'info') {
- specificLevelStyle = styles.info;
- } else if (level === 'warning') {
- specificLevelStyle = styles.warning;
- } else {
- specificLevelStyle = styles.error;
- }
- return (
- <View style={styles.container}>
- {this.props.isEditing && <CheckBox onValueChange={(value) => toggleItemCheck(item, value)} style={styles.checkbox} value={isChecked} />}
- <Text style={styles.detail}>{detail}</Text>
- <View style={[basicLevelStyle, specificLevelStyle]}></View>
- </View>
- );
- }
- }
特别是, 每一项是否被 check, 这个状态其实来自于 todoList 数据源, 而每一项的 Checkbox 的 value 完全受控于父组件传来的值, 所以这种用户输入型的组件, 其值完全受控于父组件的 props 的传值的, 也常被称为受控组件(Controlled Component).
另外, todoList 的每一项, 我们用 level 来表示待办项的某种等级, 用 detail 表示它的内容, 用 isChecked 来表示它是否完成.
但是做了这么多, 我们还啥都没看到呢. 所以, 接下来的关键一步, 就是把 ToDoListMain 和 ToDoListAdd 的渲染逻辑一口气写到 App.js 中去.
步骤 4, 写最外层的渲染逻辑. 在 App.js 中, 引入
- import ToDoListMain from './ToDoListMain';
- import ToDoListAdd from './ToDoListAdd';
然后定义 App 组件
- export default class App extends Component<Props> {
- constructor(props) {
- super(props);
- this.state = {
- current: 'main',
- todoList: [
- {
- level: 'info',
- detail: '一般的任务',
- isChecked: false,
- key: '0'
- },
- {
- level: 'warning',
- detail: '较重要的任务',
- isChecked: false,
- },
- {
- level: 'error',
- detail: '非常重要且紧急的任务',
- isChecked: false,
- key: '2'
- }
- ]
- }
- this.onAddItem = this.onAddItem.bind(this);
- this.onBack = this.onBack.bind(this);
- this.toggleCheckAll = this.toggleCheckAll.bind(this);
- this.toggleItemCheck = this.toggleItemCheck.bind(this);
- }
- onAddItem() {
- this.setState((prevState) => {
- return {
- current: 'add'
- }
- });
- }
- onBack() {
- this.setState({
- current: 'main'
- });
- }
- toggleCheckAll() {
- const flag = this.isAllChecked();
- const newTodos = this.state.todoList.map(item => {
- return {
- ...item,
- isChecked: !flag
- };
- });
- this.setState({
- todoList: newTodos
- });
- }
- toggleItemCheck(item, key) {
- const newTodos = this.state.todoList.map(todo => {
- if (todo !== item) {
- return todo;
- }
- return {
- ...todo,
- isChecked: !item.isChecked
- }
- });
- this.setState({
- todoList: newTodos
- });
- }
- isAllChecked() {
- if (!this.state.todoList) return false;
- if (this.state.todoList.length === 0) return false;
- return this.state.todoList.every(item => item.isChecked);
- }
- render() {
- if (this.state.current === 'main') {
- return (<ToDoListMain
- isAllChecked={this.isAllChecked()}
- toggleCheckAll={this.toggleCheckAll}
- toggleItemCheck={this.toggleItemCheck}
- onAddItem={this.onAddItem}
- todoList={this.state.todoList} />);
- } else {
- return (<ToDoListAdd onBack={this.onBack} />);
- }
- }
- }
我们看到 App 组件有两个状态, 一个是 current, 用以指定当前渲染的是哪个界面(其实我们这里就两个界面). 另一个是 todoList 数据源.
界面是如何切换的呢?
观察 render 函数, 里面就是界面渲染逻辑, 如果 this.state.current 的值是 main, 那么就会渲染 App 就会渲染 ToDoListMain, 否则, 渲染 ToDoListAdd. 你可以理解成, 我们手动实现了一个特别简单的前端路由. 这一切都是基于当 state 变化时, 相应的界面自动重新渲染了.
更具体地来说, 我们把 onAddItem 作为 props 的一个属性传给 ToDoListMain, 把 onBack 也作为一个属性传给 ToDoListAdd. 所以当它们的头部相应文字被点击时, 实际上调用的, 是定义在 App 组件中的回调函数. 回调函数修改了 current 状态, 而 current 状态的修改引起了 App 的 render 函数重新被调用, 它根据当前的 current 状态而重新渲染了相应的界面.
todoList 中每项的 key 值是给 FlatList 作为唯一标识用的.
另外, 在 setState 句子中, 我们会构造一个新的变量, 然后一把 setState, 而不是去修改原有的 state. 这也是 RN 中的常用做法. 对于初学者来说, 可能语法有点怪异. 不过, 这样做是有它的理由的. 简单的说, 因为 RN 在底层大量使用了比较对象是否变化的逻辑, 如果挨个便利对象的每个属性, 而且对象很复杂的话, 这个比较的逻辑是很慢的. 但是, 比较两个对象的引用是否相等却很容易, 直接一个表达式就可以了. 所以, 我们在 setState 时往往会构造一个新的对象. 更深的机理就留给读者去探索啦.
好了, 让我们运行起程序, 看看效果怎么样吧.
本文通过一个 ToDo List 的例子, 介绍了 RN 中最基本的两个概念 state 和 props. 并简单实现了状态提升, 组件间的通信等功能.
不过这个例子还没完. 这个 ToDo List 目前只是一个展示的功能, 如何对它们进行编辑, 添加, 删除, 后续会进一步分享.
文章中使用到的源码下载: https://files.cnblogs.com/files/powertoolsteam/todo-list.zip
来源: https://www.cnblogs.com/powertoolsteam/p/react-native-tutorials3.html