原文链接:https://blog.csdn.net/hl582567508/article/details/76982756
redux中文文档:http://cn.redux.js.org/
React项目开发中的数据管理
对于React的初学者在项目开发中常常还是会以DOM操作的思维方式去尝试获取、修改和传递数据,但是这种思想,在React思想中显然是错误的,针对这种情况下文将进行一个简易的总结。我们将从基础的纯React组件之间的传值开始论述,然后分析React结合Redux之间的数据传递,以及最后基于dva脚手架的数据传输问题进行探讨和分析。
一、 原生React组件的数据传输管理:
原生React组件之间的数据传输主要依赖于两个关键词:属性(props) 和状态(state)。每一个组件都是一个对象,props是对象的一个属性,组件对象可以通过props进行传递。React 的核心思想是组件化的思想,应用由组件搭建而成,而组件中最重要的概念是State(状态),State是一个组件的UI数据模型,是组件渲染时的数据依据。state与props的最大区别在于props是不可变的而state是可变的。具体内容后面会详细讲解。
原生React组件之间数据传递场景可以分为以下四种:
- 组件内部的数据传输
- “父组件”向“子组件”传值
- “子组件”向 “父组件”传值
- “兄弟组件”之间的传值
- 组件内部的数据传输
在初学过程的项目开发中常常会有去尝试DOM操作的冲动,虽然大部分情况下这种尝试是错误的,但是在某些时候还是不得不需要获取对DOM的值进行操作。例如:点击一个按钮之后触发一个点击事件,让一个input文本框获得焦点。jQuery开发者的第一反应肯定是给button绑定点击事件,然后在事件中通过$(‘select’)获取到要操作的节点,再给节点添加焦点。然而在React中这种操作是不允许的,而React中应该怎么做呢?
React Refs属性:
import React, { Component } from 'react';
class MyComponent extends Component({
handleClick = () => {
// 使用原生的 DOM API 获取焦点
this.refs.myInput.focus();
},
render: function() {
// 当组件插入到 DOM 后,ref 属性添加一个组件的引用于到 this.refs
return (
<div>
<input type="text" ref="myInput" />
<input
type="button"
value="点我输入框获取焦点"
onClick={this.handleClick}
/>
</div>
);
}
});
ReactDOM.render(
<MyComponent />,
document.getElementById('example')
);
我们可以从上面的代码当中看到 其中的ref在功能上扮演起了一个标识符(id)的角色,this.refs.myInput.focus()也有一种document.getElementById(‘myInput’).focus()的味道。
上面的操作我们也称为React表单事件,React表单事件中除了ref具有关键作用外,还有另一个关键参数’event’。例如:当我需要实时获取到一个文本框里面的内容,然后进行一个判断,当满足某个条件的时候触发另一个事件。这个时候就需要使用到这个一个关键参数’event’。
React 表单事件-event参数:
class MyComponent extends Component{
handleChange = (event) => {
if(event.target.value === 'show'){
console.log(this.refs.showText);
}
};
render(){
return(
<div>
<input type="text" onChange={this.handleChange}/>
<p ref='showText'>条件满足我就会显示在控制台</p>
</div>
)
}
}
export default MyComponent;
上面实例实现的效果就是,通过event.target.value获取当前input中的内容,当input中输入的内容是show的时候,控制台就将ref为showText的整个节点内容打印出来。从这个实例当中我们也看到了,event作为一个默认参数将对应的节点内容进行了读取。
因此在组件内部涉及的DOM操作数据传递主要就是这两种方式,可以根据不同的场景选择不同的方式。虽然ref适用于所有组件元素,但是ref在正常的情况下都不推荐使用,后面会进行介绍通过 state管理组件状态,避免进行DOM的直接操作。
- “父组件”向“子组件”传值
父组件与子组件之间的通信通常使用props进行。具体如下:
import React,{ Component } from 'react'
class ChildComponent extends Component{
render (){
return (
<div>
<h1>{this.props.title}</h1>
<span>{this.props.content}</span>
</div>
)
}
}
class ParentComponent extends Component {
render (){
return (
<div>
<ChildComponent title="父组件与子组件的数据传输测试" content="我是传送给子组件span中显示的数据" />
<p>我是父组件的内容</p>
</div>
)
}
}
export default ParentComponent;
上面示例展示了父组件向子组件传递了两个props属性分别为title和content,子组件通过this.props获取到对应的两个属性,并将其展示出来,这个过程就是一个父与子组件之间的数据交互方式。但是也可以从例子中看到props的值是不变的,父传给子什么样的props内容就只能接收什么样的使用,不能够在子中进行重新赋值。
- “子组件”向 “父组件”传值
本例中将会引入了管理组件状态的state,并进行初始化。具体如下:
import React, { Component } from 'react';
//子组件
class Child extends Component {
render(){
return (
<div>
请输入邮箱:<input onChange={this.props.handleEmail}/>
</div>
)
}
}
//父组件,此处通过event.target.value获取子组件的值
class Parent extends Component{
constructor(props){
super(props);
this.state = {
email:''
}
}
handleEmail = (event) => {
this.setState({email: event.target.value});
};
render(){
return (
<div>
<div>用户邮箱:{this.state.email}</div>
<Child name="email" handleEmail={this.handleEmail}/>
</div>
)
}
}
export default Parent;
通过上面的例子可以看出”子组件”传递给”父组件”数据其实也很简单,概括起来就是:react中state改变了,组件才会update。父写好state和处理该state的函数,同时将函数名通过props属性值的形式传入子,子调用父的函数,同时引起state变化。子组件要写在父组件之前。
从本示例中也可以看出state可以通过setState进行重新赋值,因此state是可变的,表示的是某一时间点的组件状态。
- “兄弟组件”之间的传值
当两个组件不是父子关系,但有相同的父组件时,将这两个组件称为兄弟组件。严格来说实际上React是不能进行兄弟间的数据直接绑定的,因为React的数据绑定是单向的,所以才能使得React的状态处于一个可控的范围。对于特殊的应用场景中,可以将数据挂载在父组件中,由两个组件共享:如果组件需要数据渲染,则由父组件通过props传递给该组件;如果组件需要改变数据,则父组件传递一个改变数据的回调函数给该组件,并在对应事件中调用。从而实现兄弟组件之间的数据传递。
import React, { Component } from 'react';
//子组件
class Child extends Component {
render(){
return (
<div>
我是子组件邮箱:<input onChange={this.props.handleEmail} defaultValue={this.props.value} />
</div>
)
}
}
//兄弟组件
class ChildBrother extends Component {
render(){
return (
<div>
我是兄弟组件:{this.props.value}
</div>
)
}
}
//父组件,此处通过event.target.value获取子组件的值
class Parent extends Component{
constructor(props){
super(props);
this.state = {
email:''
}
}
handleEmail = (event) => {
this.setState({email: event.target.value});
};
render(){
return (
<div>
<div>我是父组件邮箱:{this.state.email}</div>
<Child handleEmail={this.handleEmail} value={this.state.email}/>
<ChildBrother value={this.state.email}/>
</div>
)
}
}
export default Parent;
上面例子中就是child组件的值改变后存储在父组件的state中,然后再通过props传递给兄弟组件childBrother。从而实现兄弟组件之间的数据传递。
二、 基于Redux的React项目开发中的数据管理
前面在分析原生React组件之间的数据传输中讲到两个关键词:state和props,在项目的实际开发过程中,这里的state可能包括服务器响应数据、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。
因此在这些问题下便产生了 Redux ,在Redux的理念中通过限制更新发生的事件和方式试图让state的变化变的可预测。Redux可以用三个基本原则来描述:
- 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- State是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
- 使用纯函数来执行修改:为了描述 action 如何改变 state tree ,你需要编写 reducers。
redux的基本工作流程为store进行管理state和reducers,reducers接收一个action和原始的state,生成一个新的state,dispatch进行触发一个action,打一个比方:store就好比是一个银行,state就是银行中存的钱,reducers就是银行的用户管理系统,dispatch就是取款机,action就是取款机发出的请求,component就是用户。所以当我们要完成一个取钱的过程,首先就是用户(component)通过取款机(dispatch)发起一个(action)取款的请求,当银行的用户管理系统(reducers)接收到请求以后,调取用户的原来的账户信息(old state),进行相应(action)操作,如果没有什么问题则更改账户信息生成新的账户资料(new state),并把钱取给用户(返回给component)。
整个流程可以通过下图表示:
我们可以来看一个简单的例子:基本功能就是在一个任务管理器中添加新的任务,我们主要看其数据走向。
Action:
let nextTodoId = 0
export const addTodo = (text) => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
Reducers:
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
]
default:
return state
}
}
export default todos
Component:
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => {
input = node
}} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
Store:
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import reducer from './reducers'
const store = createStore(reducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
上面示例就是一个简易的react-redux项目中的数据请求与处理,component发起dispatch(addTodo(input.value))请求,reducers接收’ADD_TODO’返回一个新的state,store进行管理整个reducers和state将其结果渲染在页面当中。
总结:
redux只是对于react的state进行了管理,对于react的props并没有进行管理,这也与props本身的特性有关,props本身就是只读属性,所以可控性比较强,不需要进行再次包装管理。前面讲的主要是针对于同步情况下的redux的请求与处理过程,并没有阐述异步情况,由于其基本思想是一样的只是异步请求需要使用redux-saga的fetch请求远程服务器,然后再接收收据后进行相应的操作。具体的流程会在后面的基于dva的React项目开发中的数据管理中进行讲解。
三、 基于dva的React项目开发中的数据管理
dva是基于 redux、redux-saga 和 react-router@2.x 的轻量级前端框架。是使用React技术栈进行前端开发的脚手架。
dva实际上并没有引入什么新的概念,依旧使用的是React、Redux、React-route技术栈的相关概念,唯一的特点就是简化了React和Redux、Redux-saga之间的数据数据交互。可以从下面的实例中来进行简要了解:
models:
import { getInputOutputProfiles,deleteInputOutputProfiles,addInputOutputProfiles } from "../../services/InputOutputManagement"
export default {
namespace : 'input_output',
state : {
...
data:[],
...
},
effects : {
*getInputOutputProfiles({ payload }, { put, call, select }) {
const type_input='REMOTEFILESHARED_INPUT';
const type_output='REMOTEFILESHARED_OUTPUT';
const token = yield select(state => state.home.token);
const result_input = yield call(getInputOutputProfiles,{payload:{token,type:type_input}});
const result_output = yield call(getInputOutputProfiles,{payload:{token,type:type_output}});
let data=[];
result_input.remoteFileList.map(value => {
data.unshift(value)
});
result_output.remoteFileList.map(value => {
data.unshift(value)
});
console.log(data);
yield put({type:'setData',payload:{ data: data }});
},
...
}
},
reducers : {
...
setData(state,{ payload:{data} }){
return { ...state, data:data }
},
...
},
subscriptions : {
setup({dispatch, history}) {
return history.listen(({pathname}) => {
if (pathname === '/system/input_output') {
dispatch({
type:'getInputOutputProfiles'
});
}
});
}
}
}
component:
class SectionPanel extends Component{
...
handleSubmit = () => {
this.props.dispatch({ type: 'input_output/addInputOutputProfiles', payload: { id:this.props.id, type:this.props.type }});
};
...
}
const mapStateToProps = ( state ) => {
return state;
};
export default connect(mapStateToProps)(SectionPanel);
从这个示例当中我们可以看到,model中进行state的定义,以及reducers的定义,在dva中reducers是唯一可以改变state的地方。从例子中我们可以看到,在subscriptions中进行了一个订阅监听,当加载pathname === ‘/system/input_output’的时候通过dispatch发起一个异步请求getInputOutputProfiles,请求会连接到服务器,从服务器端获取相应的数据,然后再对数据进行处理,再执行reducers中的同步setData:yield put({type:’setData’,payload:{ data: _temp }});改变当前state中的data数据。有了data数据,组件就可以遍历数据呈现给用户。
这是一个由订阅数据源而发起的一个改变state的方式,除此之外,state改变和去向主要应用在组件当中,如上component当中所示,组件中需要使用state,首先要进行state和props的映射,然后组件就可以通过this.props进行获取相应的state值,因此通过mapStateToProps方法进行映射,然后通过connect方法将映射的结果与组件绑定,此处需要知道的是组件中发起请求的dispatch也是需要将组件与redux连接(connect)之后才能在组件中使用dispatch。这些准备工作做好之后便可以在组件中发起dispatch请求改变state状态了。
从上面的示例中我们会发现在dva中不需要显式的编写action,也不用写创建store的过程,而是在dispatch中将传递action名改变为对象,对象包含两个部分{ type:”,payload:{ } },具体触发reducers的过程以及生成新的state的具体操作都是由dva内部进行,从而简化了操作。
以上便是一个dva项目的数据传递流,下面我以图的形式进行展示:
总结
从原生React到react-redux再到dva其思想上实际并没有本质上的颠覆,redux简化react的数据管理,dva简化react-redux项目的数据管理,dva最终的目的其实也只有一个,就是写更少的代码做更多的事情。