react进阶-redux
开始
Redux
,一种新型的前端“架构模式”。经常和 React.js
一并提出,你要用 React.js
基本都要伴随着 Redux
和 React.js
结合的库 React-redux
。
要注意的是,
Redux
和React-redux
并不是同一个东西。Redux
是一种架构模式(Flux 架构的一种变种),它不关注你到底用什么库,你可以把它应用到React
和Vue
,甚至跟jQuery
结合都没有问题。而React-redux
就是把Redux
这种架构模式和React.js
结合起来的一个库,就是Redux
架构在React.js
中的体现。
安装redux
和 react-redux
npm i -S redux react-redux
那么如何去使用它们? 首先得了解provider
组件,和connect
以及store
,action
,reducer
.
解析 redux
redux
中含有store
,action
,reducer
.redux
的作用是什么? 它们是如何工作的.
redux
的作用就是用来管理数据状态的.
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。
store
,action
,reducer
是如何协调工作的?
假设我们有一个事情清单要维护:
const state = {
todos: [
{
text: 'eat food',
completed: true,
},
{
text: 'Exercise',
completed: false
},
]
};
我们如何去维护这些数据? 比如增加一个要做的事情.
而reducer
就是做这个工作的,它完成了数据的初始化,并且决定了对这个数据有哪些行为.并且它还是一个纯函数(即不对参数进行修改的函数),要保证只要输入的参数一样,那么结果一定相同.
const defaultState = {
todos: [],
}
function todoReducer(state=defaultState, action) {
switch (action.type) {
case 'insert':
const todo = { text: action.text, completed: false };
return { ...state, todos: [...state.todos, todo] };
default:
return { ...state };
}
}
我们只能够通过这个todoReducer
来操作数据,以上我们需要传入行为action
,定义操作的类型以及数据.显然这样通过字符串的形式来判断行为,是不行的.所以我们可以预先定义action
,然后再传入,可以联想到设计模式中的工厂模式.
action.js
:
export const actionType = {
insert: 'insert',
}
export const insertTodo = (value)=>({
type: actionType.insert,
text: value,
});
acitonType
规定了aciton
的有那些行为,并且提供了一些action
,这里我们演示只有一个增加的action
.
然后我们reducer
就能够写成:
import { actionType } from './action';
const defaultState = {
todos: [],
}
function todoReducer(state = defaultState, action) {
switch (action.type) {
case actionType.insert:
const todo = { text: action.text, completed: false };
return { ...state, todos: [...state.todos, todo] };
default:
return { ...state };
}
}
export default todoReducer;
这样子的话,我们可以通过获取action
,给reducer
来完成我们对数据的操作.
那么我们如何获取state
数据? 如果有一个getter
函数就好了,这就是store
,并且它还封装了reducer
.
store.js
import todoReducer from './todoReducer';
function createStore(reducer) {
let state = undefined;
const dispatch = (action) => {
state = reducer(state, action);
}
// 初始化state
dispatch({});
const getState = () => state;
return { getState, dispatch };
}
const store = createStore(todoReducer);
export default store;
这里的store
有获取state
的方法getState
,还有对其操作的函数dispatch
.故我们只要通过store
就能够对数据进行操作.
示例:
import store from './store';
import { insertTodo } from './action';
import { Component } from 'react';
class Test extends Component {
constructor(props) {
super(props);
console.table(store.getState());
store.dispatch(insertTodo('run'));
store.dispatch(insertTodo('write a blog'));
console.table(store.getState());
}
render() {
return (null);
}
}
export default Test;
这里我们增加了两个事件,验证一下:
这就是redux
的工作原理,还有一个问题就是store
就像是全局变量,可能两个组件的数据变量可能会重复,会出现不方便维护,解决方案是我们为每个组件设置成单独的数据域,就好像:
{
app:{...},
todoList: {...},
...
}
类似这种效果,这样也不会产生数据的污染.
redux
恰好就提供了combineReducers
函数来实现这个效果.它就是结合reducer
来为索引.
先创建一个总的 reducer
:
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
const rootReducer = combineReducers({
todoReducer,
});
export default rootReducer;
这里将刚才的todoReducer
放进去总reducer
.
然后我们的store
引入该reducer
:
import rootReducer from './rootReducer';
import { createStore } from 'redux';
const store = createStore(rootReducer);
export default store;
createStore
,是redux
已经帮我们写好了,我们直接用就行.
constructor(props) {
super(props);
console.table(store.getState());
store.dispatch(insertTodo('run'));
store.dispatch(insertTodo('write a blog'));
console.table(store.getState());
}
我们同样的对其进行打印,看看数据是否有变化.
我们可以看到,store
保存的数据结构已经发生了变化,已经开始分区域保存数据,实际保存数据的其实是reducer
,只不过store
包含了所有的reducer
.
结合 react-redux
React-Redux
是Redux
的官方React
绑定库。它能够使你的React
组件从Redux store
中读取数据,并且向store
分发actions
以更新数据.
react-redux
提供了两个东西,<Provider/>
组件和connect
函数.
React-Redux
将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。UI 组件:
只负责 UI 的呈现,不带有任何业务逻辑,没有状态(即不使用this.state这个变量,所有数据都由参数(this.props)提供,
不使用任何 Redux 的 API.
容器组件:
容器组件的特征恰恰相反,负责管理数据和业务逻辑,不负责 UI 的呈现,带有内部状态,使用 Redux 的 API
Provider
React-Redux
提供<Provider/>
组件,能够使你的整个app
访问到Redux store
中的数据.
源码:
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
可以看到其作用就是将store
绑定在了上下文对象context
,然后子组件就可以通过context
获取到store
.
connect()
connect
方法,用于从 UI
组件生成容器组件.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
render () {
const { store } = this.context
let stateProps = mapStateToProps(store.getState())
// {...stateProps} 意思是把这个对象里面的属性全部通过 `props` 方式传递进去
return <WrappedComponent {...stateProps} />
}
}
return Connect
}
它从context
获取了store
,并且将store
里的数据,通过props
注入到了组件,所以组件可以通过props
里读取store
里的数据,而mapStateToProps
,顾名思义就是将store
里的state
映射到组件的props
.
还能继续深入对dispatch
进行映射,同样映射到props
里,并且数据dispatch
之后要重新渲染组件.
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super()
this.state = {
allProps: {}
}
}
componentWillMount () {
const { store } = this.context
this._updateProps()
// 订阅,只要dispatch就会调用传入的函数
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {} // 防止 mapStateToProps 没有传入
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {} // 防止 mapDispatchToProps 没有传入
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
这样就也就完成了state
和dispatch
映射到props
的过程.
实际上react-redux
已经封装了该函数,所以我们直接用就行了.
示例: 将 redux
和react
结合起来,图像化.
import React from 'react';
import TodoList from './components/react-redux/TodoList';
import { connect } from 'react-redux';
import { insertTodo } from './store/action'
class App extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
handleChange = (e) => {
const value = e.target.value;
this.setState({
value,
});
}
handleClick = () => {
this.props.insertTodo(this.state.value);
this.setState({
value:'',
});
}
render() {
return (
<div>
<input value={this.state.value} onChange={this.handleChange} />
<button onClick={this.handleClick}>add</button>
<TodoList todos={this.props.todos} />
</div>
);
}
}
const mapToStateToProps = (state) => {
return {
todos: state.todoReducer.todos,
}
}
const mapToDispatchToProps = (dispatch) => {
return {
insertTodo: (value) => { dispatch(insertTodo(value)) },
}
}
export default connect(mapToStateToProps, mapToDispatchToProps)(App);
这里我们返回的是一个由connect
生成的容器组件.并且完成了映射,将state.todoReducer.todos
映射到了props.todos
上,dispatch
也是如此.所以在组件里,我们直接用props
调用就行了,TodoList
就是简单的react
组件.
附上TodoList.js
:
import React, { Component } from 'react';
import Todo from './Todo';
class TodoList extends Component {
render() {
const { todos } = this.props;
return (
<ul>
{todos.map(todo => {
return <Todo todo={todo} />;
})}
</ul>
);
}
}
export default TodoList;
以及Todo.js
:
import React, { Component } from 'react';
class Todo extends Component {
render() {
const { todo } = this.props;
return (
<div>
{todo.text}
{todo.completed ? <input type='checkbox' checked />
: <input type='checkbox' />}
</div>
);
}
}
export default Todo;
记住一定要
connect
生成的组件一定要在<Provider/>
下才能有效,因为connect
要通过context
获取store
,而store
放在context
的过程是有<Provider/>
,前面已经讲过了.
import { Provider } from 'react-redux';
import store from './store/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));
最后的效果: