React 我们只是数据的搬运工,从自然规律到设计,编码的一些发散
React 状态管理
状态管理符合事物转换的一般规律
我们都知道大气中水循环,降水,冰雪融化等使地面形成河流,河流中的水因重力流向湖泊,海洋,蒸发形成云雨,受风力影响,云雨移动又形成降水,如此循环往复。React 组件中的数据流动其实跟这个水循环也是极其相似的,所以只要能理解这个水循环,那么 React 的数据流动就能比较轻易的明白。
我们来对比看看这个水循环大致模拟图与 React 数据流向图 :
我们从这两个结构图中,能感受到这个过程是否极其相似,那么就可以把蒸发,风力看成事件属性,useState看成雨云冷凝形成降水,这不是巧合,这符合事物转换的一般自然规律。你们想想水从气态到液态再到固态,再反向转换是不是也是这个道理。
有了这样的想法,我们来看看代码的实现,就更清楚了,还是延用之前用到的周计划的项目,之前的项目是只是一个静态展示,现在用到状态管理让数据动起来。
创建一个可操作的周计划
- 创建 weekPlanList.json 数据源:
[
{
"title": "周一的计划",
"index": "1",
"todoItems": [
{
"index": "1-1",
"time": "7:00 PM",
"matters": "运动健身"
},
{
"index": "1-2",
"time": "9:00 PM",
"matters": "阅读"
}
]
},
{
"title": "周二的计划",
"index": "2",
"todoItems": [
{
"index": "2-1",
"time": "7:00 PM",
"matters": "练习萨克斯"
},
{
"index": "2-2",
"time": "8:00 PM",
"matters": "看一部电影"
}
]
},
{
"title": "周三的计划",
"index": "3",
"todoItems": [
{
"index": "3-1",
"time": "7:00 PM",
"matters": "练习毛笔字"
},
{
"index": "3-2",
"time": "8:00 PM",
"matters": "短距离夜骑"
}
]
},
{
"title": "周四的计划",
"index": "4",
"todoItems": [
{
"index": "4-1",
"time": "7:00 PM",
"matters": "运动健身"
},
{
"index": "4-2",
"time": "8:00 PM",
"matters": "高数学习"
}
]
},
{
"title": "周五的计划",
"index": "5",
"todoItems": [
{
"index": "5-1",
"time": "7:00 PM",
"matters": "云顶之奕"
}
]
},
{
"title": "周六的计划",
"index": "6",
"todoItems": [
{
"index": "6-1",
"time": "7:00 AM",
"matters": "绿道骑行"
},
{
"index": "6-2",
"time": "7:00 PM",
"matters": "线代学习"
}
]
},
{
"title": "周日的计划",
"index": "7",
"todoItems": [
{
"index": "7-1",
"time": "8:00 AM",
"matters": "学习,整理,总结"
},
{
"index": "7-2",
"time": "3:00 PM",
"matters": "追番,追剧,追综艺"
}
]
}
]
- 创建 TodoItem.js 组件:
/**
* TodoItem.js
* 每天待处理项
* @param {*} param0
* @returns
*/
function TodoItem ({matters, time, checked, index, checkChange = f => f}) {
return(
<div className="check-item">
<label htmlFor="input">{ `${time} : ${matters}` }</label>
<input
type="checkbox"
checked={checked}
value={index}
style={{marginLeft: '6px'}}
onChange={(e) => {checkChange(e.target.value)}}
/>
</div>
)
}
export default TodoItem;
- 创建 TodoItems.js 组件:
/**
* TodoItems.js
* 每天待处理项列表
* @param {*} param0
* @returns
*/
import TodoItem from "./TodoItem";
function TodoItems ({todoItems, checkedIndexList, checkChange = f => f}) {
return (
<ul>
{todoItems.map(item => (
<li key={item.index} >
<TodoItem {...item} checked={checkedIndexList.includes(item.index)} checkChange={checkChange} />
</li>
))}
</ul>
)
}
export default TodoItems;
- 创建 WeekList.js 组件:
/**
* WeekList.js
*/
import React from 'react';
import TodoItems from "./TodoItems";
function WeekList({weekList, checkedIndexList, checkChange}) {
return weekList.map((week, index) => (
<React.Fragment key={index}>
<div className="week-title">{ week.title }</div>
<div className="items-container">
<TodoItems {...week} checkedIndexList={checkedIndexList} checkChange={checkChange} />
</div>
</React.Fragment>
))
}
export default WeekList;
- 创建 App.js 组件:
import { useState } from 'react';
import WeekList from './components/WeekList';
import weekplanList from './source/weekPlanList.json';
import './App.css';
const weekPlans = [...weekplanList];
function App() {
const [checkedIndexList, setCheckedIndexList] = useState([]);
const checkChange = (val) => {
let checkedList = checkedIndexList.filter(checkedIndex => checkedIndex !== val);
if (checkedList.length === checkedIndexList.length) {
checkedList = [...checkedList, val];
}
setCheckedIndexList(checkedList);
}
return (
<div className="App">
<div className="plan-head">周计划管理</div>
<div className="plan-body">
<WeekList weekList={weekPlans} checkedIndexList={checkedIndexList} checkChange={checkChange}></WeekList>
</div>
</div>
);
}
export default App;
经过如上代码的编写,我们可以得到如下视图以及交互:
周计划代码解读
先说说我最开始创建这个项目的数据结构是怎样的吧?最开始,考虑到为每一个代办事项添加一个 checked 属性,然后绑定对应的 checked 属性,最开始我也是这么做的,如果这么做会有什么样的结果呢?我们来看一下:
TodoItem.js 文件中,那么checked 属性绑定对应的 checked,但是我们在变更事件 onChange 中,要让 App 组件中知道是哪个代办项发生了变化,那么我们就必须要把待办项 唯一键 index 属性, 以及当天的 index 属性回传,达到通知 useState 进行具体的改变,这样我们需要历遍 weekPlans 改变才能改变代办项的 checked 状态。这样的设计,有没有发现什么弊端呢?
- 回传参数复杂,还需要历遍去找当天的唯一键 index
- 历遍数据去更改数据源以达到更新 DOM 的效果
- 加上更新 DOM 的历遍,我们在重复的去做历遍操作
或许这个时候我们就在想,这样设计这个组件是否恰当,我们再来分析一下现在的这种数据模式。
- 更新选中的 index 的值,可以做及时的更新
- 重新渲染 DOM 的过程中,借助渲染 DOM 的历遍来进行数据处理,不需要进行多次的历遍
- 不会污染源数据,只针对选择的 index 进行处理
- 回传参数只需要找对当前处理的待办项 index 进行简单的增删操作
像这样的模式,其实我们在开发中也会经常遇到,像列表数据的选择,树形下拉菜单的选择等等,很多模式都是利用的这个。
大概的设计已经清楚了,我们来看看这个 useState 的作用:
const [value, setValue] = useState(initValue);
- useState 函数接受一个初始值参数,返回一个数组;
- 数组的第一个元素 value 就是需要交互发生改变的值,初始化的时候 initValue 会被赋值给 value;
- 第二个元素 setValue 方法就是通过函数处理返回一个值,这个值作为第一个元素 value 的值。
值得注意的是,我们在处理数据变化的时候(使用 useState 的时候),我们是在根组件(App.js)中,为什么不能是在 WeekList 或是其他中间组件,如果一整条河被污染了那我们首先应该检测,处理的是不是源头,是不是也是这样依次往下进行排查?一句话就是,把状态改变放在数据源头位置,有利于我们对问题的排查,也有利于上级组件需要使用的时候再返工去给上级设置状态,数据流动性思维很重要。
如果你学习过 Vue,这样的描述,我们是不是很容易联想到 Vue 的 computed 计算属性中的 get,set 方法,那这样就更好理解了。插一句,我们在学习的过程中,学习到新的知识点的时候,要多思考现在的这个知识点跟原来熟悉,学过的知识点的一些关联,对比,相似等特性,这样对我们的理解,记忆,使用都会有很多的帮助,学习是先扩散再聚拢的。
在 App 组件中有这样一段代码我觉得可以拿来讲解一下,并不是说这代码很厉害,很不同寻常,我只是想让大家在写代码的时候要多思考。
const checkChange = (val) => {
let checkedList = checkedIndexList.filter(checkedIndex => checkedIndex !== val);
if (checkedList.length === checkedIndexList.length) {
checkedList = [...checkedList, val];
}
setCheckedIndexList(checkedList);
}
这段代码也就是在对选择的待办项的唯一键 index 进行增、删的操作。想必很多朋友在写这个代码的时候的设计思路如下:
先在 checkedList 中判断是否存在当前选择的项,如果存在做删除操作,如果不存在,则做新增操作;
这样思考肯定没有问题,我反而觉得你逻辑非常严谨,可以说是密不透风,我们来对比一下代码看看呢?
const checkChange = (val) => {
const hasIndex = checkedIndexList.includes(val);
let checkedList = [];
if (hasIndex) {
const index = checkedIndexList.findIndex(checkedIndex => checkedIndex === val);
// 友情提醒:splice 方法会改变原数组,所以很多时候用 filter 更好一些
checkedList = [...checkedIndexList];
checkedList.splice(index, 1);
} else {
checkedIndexList = [...checkedIndexList, val];
}
}
对比于 App 组件中的代码,跟我们平常写的差别是不是一下就出来了,App 组件中,先不判断是否存在当前处理的 index,而是直接删除,再比较删除前后的数组的长度来判断之前的操作是否为删除操作,如果长度相同,那么之前的操作就可以看做是一个赋值操作,再进行一个新增操作,搞定,如果长度不同,那么刚才的操作肯定就是一个删除操作,那么数据处理直接结束。当我们看到这两种模式的时候,是否能联想起刚学 JavaScript 时的一对循环,(while...do... 和 do...while);
总结
- 通过一个大气水循环去理解 React 中数据流方式,把父组件看做高海拔,把数据看做水,那么 React 数据流也符合一般自然规律;
- 通过一个计划勾选来理解 useState 在 React 中对数据的控制,数据流动性思维对 useState 使用在什么位置提供了一个使用的依据;
- 通过一个案例分析,来思考推导出组件的设计,模式的设计对于开发,学习的思维的发散,学习要关联,对比,先发散再聚合,编码要思考如何设计更优更简性能更好的代码;
转载自:https://juejin.cn/post/7270061728897007674