Redux is a predictable state container for JavaScript apps.

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

这是 redux 官网的原话

入门 Redux-编程知识网
我最早接触的框架是 Vue,这个图我自己觉得和 vuex 好像有一些相似的地方。

我们今天就写一个TODOList 的小 demo ,来入门学习一下 redux
(为了效果好看,使用 antd-ui 组件库)


1. 初始化项目

安装官方脚手架之后

mkdir ReduxDemo
cd ReduxDemo
create-react-app demo
cd demo
npm start

对的,就是这么简单

不出意外,就能看到平常你看见的样子了。

项目精简

src 目录下只留下一个 index.js,新建一个 TodoList.js

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList'ReactDOM.render(<TodoList />,document.getElementById('root')
);

TodoList.js

import React, { Component } from 'react';
class TodoList extends Component {render() {return (<div>Hello World</div>);}
}
export default TodoList;
安装UI组件库

(不想安装也行,就是样子很不好看,功能没有多大影响)
官网地址 https://ant.design/docs/react/use-with-create-react-app-cn

npm install antd --save

2. 编写基本界面

TodoList.js 引入

import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input } from 'antd'class TodoList extends Component {render() {return (<div><div><Input placeholder='请输入' style={{ width: '250px' }} /></div></div>);}
}
export default TodoList;

不出意外,你得页面上面,会出现一个 input 输入框

增加按钮

import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button } from 'antd'class TodoList extends Component {render() {return (<div style={{margin:'10px'}}><div><Input placeholder='请输入' style={{ width: '250px' }} /><Button type="primary">添加</Button></div></div>);}
}
export default TodoList;

一个按钮就可以了。接下来是 List 列表(先使用假数据)

在 class 外面声明一个 list 数组

const data=['react','vue','ang'
]

增加 List 组件

import { Input, Button, List } from 'antd'
<div style={{ margin: '10px' }}><div><Input placeholder='请输入' style={{ width: '300px' }} /><Button type="primary">添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={data}renderItem={item => (<List.Item>{item}</List.Item>)}/></div>
</div>

入门 Redux-编程知识网
对的,不出意外,你的页面应该是长这个样子的

3. 正式进入 redux,创建 store 和 reducer

Redux工作流程中有四个部分,最重要的就是store这个部分,因为它把所有的数据都放到了store中进行管理。在编写代码的时候,因为重要,所以要优先编写store。

想使用,先安装

npm install --save redux

在 src 目录下,创建 store 文件夹。文件夹下创建一个index.js文件。index.js就是整个项目的store文件

index.js

import { createStore } from 'redux'
const store = createStore()
export default store

从 redux 中导出 createStore ,使用 createStore方法创建一个 store仓库,之后进行导出

这个仓库创建好了,需要一位管理员来管理这个仓库,Reducers 就是这么一位天选之子。同级目录下创建 reducer.js 文件

reducer.js

const defaultState = {}
export default (state = defaultState, action) => {return state
}

这位管理员有一个本子 state,里面记载着仓库的一切。同时我们给这个本子添加一个初始的内容 defaultState,返回这个本子上面的内容

把reducer引入到store中,再创建store时,以参数的形式传递给store。

import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
export default store
转移数据

把刚刚在页面中定义的 list 数组迁移到 reducer 中,给这个记录着仓库一切的本子赋上初始值

const defaultState = {inputValue: '请输入',list: ['react','vue','ang']
}
export default (state = defaultState, action) => {return state
}
获取数据

仓库里有东西,就可以获取了。

todoList 组件要使用 store,就在 src/TodoList.js 文件夹中,进行引入。在 constructor 中打印 store 里面的数据

constructor(props) {super(props);console.log(store.getState())}

入门 Redux-编程知识网
控制台已经打印出我们需要的数据,接下来我们进行赋值

import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button, List } from 'antd'
import store from './store/index'class TodoList extends Component {constructor(props) {super(props);console.log(store.getState())this.state = store.getState()}render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.state.inputValue} style={{ width: '300px' }}/><Button type="primary">添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.state.list}renderItem={item => (<List.Item>{item}</List.Item>)}/></div></div>);}
}
export default TodoList;

4. TodoList 的变化引起 redux 变化

我们给输入框增加一个 onChange 事件

<Inputplaceholder={this.state.inputValue}onChange={this.changeInputValue}style={{ width: '300px' }}
/>

定义这个事件,同时在 constructor 中进行this的绑定,修改this的指向。

constructor(props) {super(props);this.state = store.getState()this.changeInputValue = this.changeInputValue.bind(this)
}
...changeInputValue(e) {console.log(e.target.value)
}

在输入框中,输入文字,可以看到控制台输出的相应的文字。好的,这就成功了。

创建 Action

想改变 Redux 里边 State 的值就要创建 Action 了。Action 就是一个对象,这个对象一般有两个属性,第一个是对 Action 的描述,第二个是要改变的值。

changeInputValue(e) {const action = {type: 'change_input_value',value: e.target.value}
}
派发 dispatch

action就创建好了,通过dispatch()方法传递给store

changeInputValue(e) {const action = {type: 'change_input_value',value: e.target.value}store.dispatch(action)
}

(这就相当于我要做一件事,做什么事呢?我想告诉仓库我想改变 input 输入框里面的值,然后我把值传递给你,怎么弄那是你的事,之后通过 dispatch 把这件事告诉仓库)
😌😌😌😌

store 的处理

store 只是一个仓库,它并没有管理能力,它会把接收到的 action 自动转发给 Reducer。

在 reducer.js 里面,打印出这两个值
入门 Redux-编程知识网

Reducer 已经拿到了原来的数据和新传递过来的数据,现在要作的就是改变store里的值。我们先判断type是不是正确的,如果正确,我们需要从新声明一个变量newState。(记住:Reducer里只能接收state,不能改变state。), 所以我们声明了一个新变量,然后再次用return返回回去。

const defaultState = {inputValue: '请输入',list: ['react','vue','ang']
}
export default (state = defaultState, action) => {if (action.type === 'changeInput') {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}return state
}

(当reducer 知道你改变 input 输入框的值,当判断类型正确的时候,它会先拷贝出一份,然后把你传递给它的值赋值给仓库里面的值, 同时返回)

组件更新

TodoList.js

constructor(props) {super(props);this.state = store.getState()this.changeInputValue = this.changeInputValue.bind(this)this.storeChange = this.storeChange.bind(this)//订阅Redux的状态store.subscribe(this.storeChange)
}
storeChange() {this.setState(store.getState())
}

给 button 添加事件

<Button type="primary" onClick={this.clickBtn}>添加</Button>
clickBtn() {const action = {type: 'addItem'}store.dispatch(action)
}
if (action.type === 'addItem') {let newState = JSON.parse(JSON.stringify(state))newState.list.push(newState.inputValue)newState.inputValue = ''return newState
}

不出意外的话,当你输入文字,点击添加的时候,下面的 list 就会出现相应的文字

删除 TodoList-item

给每一个 ListItem 添加一个 deleteItem 事件

<ListbordereddataSource={this.state.list}renderItem={(item, index) => (<List.Item onClick={this.deleteItem.bind(this, index)}>{item}</List.Item>)}
/>
deleteItem(index) {console.log(index)const action = {type: 'deleteItem',index}store.dispatch(action)
}

点击出现相对应的序号,就是成功了。

if (action.type === 'deleteItem') {let newState = JSON.parse(JSON.stringify(state))newState.list.splice(action.index,1)return newState
}

点击各项,成功删除

来到这一步,我们就成功实现了。完美撒花 🎉🎉🎉🎉🎉🎉

5.代码优化

写 Redux Action 的时候,我们写了很多 Action 的派发,产生了很多 Action Types,如果需要Action的地方我们就自己命名一个Type,会出现两个基本问题:

  • 这些 Types 如果不统一管理,不利于大型项目的服用,设置会长生冗余代码。
  • 因为 Action 里的 Type,一定要和 Reducer 里的 type一一对应在,所以这部分代码或字母写错后,浏览器里并没有明确的报错,这给调试带来了极大的困难。

store 文件夹下,新建一个 actionTypes.js

export const  CHANGE_INPUT = 'changeInput'
export const  ADD_ITEM = 'addItem'
export const  DELETE_ITEM = 'deleteItem'

TodoList.js

//..
import {CHANGE_INPUT,ADD_ITEM,DELETE_ITEM,
} from './store/actionTypes'class TodoList extends Component {constructor(props) {// ...}render() {//....}storeChange() {this.setState(store.getState())}changeInputValue(e) {const action = {type: CHANGE_INPUT,value: e.target.value}store.dispatch(action)}clickBtn() {const action = {type: ADD_ITEM}store.dispatch(action)}deleteItem(index) {console.log(index)const action = {type: DELETE_ITEM,index}store.dispatch(action)}
}
export default TodoList;

reducer.js

import {CHANGE_INPUT,ADD_ITEM,DELETE_ITEM
} from './actionTypes'const defaultState = {inputValue: '请输入',list: ['react','vue','ang']
}
export default (state = defaultState, action) => {if (action.type === CHANGE_INPUT) {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}if (action.type === ADD_ITEM) {let newState = JSON.parse(JSON.stringify(state))newState.list.push(newState.inputValue)newState.inputValue = ''return newState}if (action.type === DELETE_ITEM) {let newState = JSON.parse(JSON.stringify(state))newState.list.splice(action.index,1)return newState}return state
}
把所有的Redux Action放到一个文件里进行管理。

在 store 文件夹下面,建立 actionCreators.js

import {CHANGE_INPUT,ADD_ITEM,DELETE_ITEM
} from './actionTypes'export default changeInputAction = (value) => {type: CHANGE_INPUT,value
}

TodoList.js

import {changeInputAction
} from './store/actionCreators'
changeInputValue(e) {const action = changeInputAction(e.target.value)store.dispatch(action)
}

改造剩下的两个

import {CHANGE_INPUT, ADD_ITEM,DELETE_ITEM
}  from './actionTypes'export const changeInputAction = (value)=>({type:CHANGE_INPUT,value
})export const addItemAction = ()=>({type:ADD_ITEM
})export const deleteItemAction = (index)=>({type:DELETE_ITEM,index
})
import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button, List } from 'antd'
import store from './store/index'import {changeInputAction,addItemAction,deleteItemAction
} from './store/actionCreators'class TodoList extends Component {constructor(props) {super(props);this.state = store.getState()this.changeInputValue = this.changeInputValue.bind(this)this.storeChange = this.storeChange.bind(this)store.subscribe(this.storeChange)}render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.state.inputValue}onChange={this.changeInputValue}value={this.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={this.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.state.list}renderItem={(item, index) => (<List.Item onClick={this.deleteItem.bind(this, index)}>{item}</List.Item>)}/></div></div>);}storeChange() {this.setState(store.getState())}changeInputValue(e) {const action = changeInputAction(e.target.value)store.dispatch(action)}clickBtn() {const action = addItemAction()store.dispatch(action)}deleteItem(index) {const action = deleteItemAction(index)store.dispatch(action)}
}
export default TodoList;

改造到这里代码依然正常运行。此时我们可以进一步改造,将 UI部分与逻辑部分开

src 目录下新建 TodoListUI.js

import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button, List } from 'antd'class TodoListUI extends Component {render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.state.inputValue}onChange={this.changeInputValue}value={this.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={this.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.state.list}renderItem={(item, index) => (<List.Item onClick={this.deleteItem.bind(this, index)}>{item}</List.Item>)}/></div></div>);}
}
export default TodoListUI;

TodoList.js

import TodoListUI from './TodoListUI'
render() {return (<TodoListUI />);
}

当然现在页面报错,我们接着进行改造

TodoList.js

<TodoListUI inputValue={this.state.inputValue}list={this.state.list}changeInputValue={this.changeInputValue}clickBtn={this.clickBtn}deleteItem={this.deleteItem}
/>

TodoListUI.js

render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.props.inputValue}onChange={this.props.changeInputValue}value={this.props.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={this.props.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.props.list}renderItem={(item, index) => (<List.Item onClick={this.props.deleteItem.bind(this, index)}>{item}</List.Item>)}/></div></div>);
}
把UI组件改成无状态组件可以提高程序性能

TodoListUI.js

import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button, List } from 'antd'const TodoListUI = (props) => {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={props.inputValue}onChange={props.changeInputValue}value={props.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={props.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={props.list}renderItem={(item, index) => (<List.Item onClick={() => {props.deleteItem(index)}}>{item}</List.Item>)}/></div></div>);
}export default TodoListUI;

这样,我们的组件就改造好了。

6.请求数据与 redux 结合

请求方式我们使用大家非常熟悉的 axios
npm install --save axios

TodoList.js

import axios from 'axios'
componentDidMount() {axios.get('http://musicapi.leanapp.cn/playlist/hot').then((res) => {console.log(res)})
}

(我自己吧,也不会搭建后台啥的,所以就使用别人的接口,演示而已)😋😋😋😋😋

入门 Redux-编程知识网
数据成功返回了。接下来就是与 redux 的联动了。

actionTypes.js

export const GET_LIST = 'getList'

actionCreators.js

import { GET_LIST } from './actionTypes'export const getListAction = (data) =>({type: GET_LIST,data
})

TodoList.js

import {getListAction } from './store/actionCreators'componentDidMount() {axios.get('http://musicapi.leanapp.cn/playlist/hot').then((res) => {const action = getListAction(res.data)store.dispatch(action)})
}

reducer.js

import { GET_LIST } from './actionTypes'const defaultState = {inputValue: '请输入',list: []
}
export default (state = defaultState, action) => {if (action.type === GET_LIST) {let newState = JSON.parse(JSON.stringify(state))console.log(action.data.tags)newState.list = action.data.tagsreturn newState}return state
}

此时我们需要对 TodoListUI.js 做一些变化,

<List.Item onClick={() => {props.deleteItem(index)}}>{item.createTime}
</List.Item>

不出意外的话,你就在页面上面看到一大堆数字。


7. redux-thunk 中间件

Redux-thunk 是这个 Redux 最常用的插件。什么时候会用到这个插件?比如在 Dispatch 一个Action 之后,到达 reducer 之前,进行一些额外的操作,就需要用到 middleware(中间件)。在实际工作中你可以使用中间件来进行日志记录、创建崩溃报告,调用异步接口或者路由。 这个中间件可以使用是 Redux-thunk 来进行增强(当然你也可以使用其它的),它是对 Reduxdispatch 的加强,

想使用,先安装

npm install --save redux-thunk

store / index.js

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const store = createStore(reducer,applyMiddleware(thunk)
)
export default store

以前 actionCreators.js 都是定义好的 action,根本没办法写业务逻辑,有了 Redux-thunk 之后,可以把 TodoList.js 中的 componentDidMount 业务逻辑放到这里来编写。也就是把向后台请求数据的代码放到 actionCreators.js 文件里。那我们需要引入 axios ,并写一个新的函数方法。(以前的action是对象,现在的action可以是函数了,这就是redux-thunk带来的好处)

actionCreators.js

import axios from 'axios'export const getTodoList = () => {return (dispatch) => {axios.get('http://musicapi.leanapp.cn/playlist/hot').then((res) => {const data = res.dataconst action = getListAction(data)dispatch(action)})}
}

TodoList.js

import { getTodoList } from './store/actionCreators'componentDidMount() {const action = getTodoList()store.dispatch(action)
}

8. React-Redux这是一个React生态中常用组件,它可以简化Redux流程

想使用,先安装

npm install --save react-redux

我们删除所有的代码,留下的东西就是我们最初简化之后的代码,src 目录下仅仅剩余一个 index.js

index.js

import React, { Component } from 'react';
class TodoList extends Component {render() {return (<div><div><input /><button>提交</button></div><ul><li>react</li></ul></div>)}
}
export default TodoList;

src 目录下新建 store 文件夹,index.js,reducer.js

index.js

import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
export default store

reducer.js

const defalutState = {inputValue: '请输入',list: []
}
export default (state = defalutState, action) => {return state
}

页面我们已经建好了。接下来就是使用 react-redux

<Provider> 是一个提供器,只要使用了这个组件,组件里边的其它所有组件都可以使用store了,这也是 React-redux 的核心组件了

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList'
import { Provider } from 'react-redux'
import store from './store'const App = (<Provider store={store}><TodoList /></Provider>
)
ReactDOM.render(App, document.getElementById('root'));

这样我们就与 react-redux 建立了连接

使用, connect 连接器的使用

映射关系就是把原来的 state 映射成组件中的 props 属性,比如我们想映射 inputValue 就可以写成如下代码。

import React, { Component } from 'react';
import store from './store'
import { connect } from 'react-redux'class TodoList extends Component {constructor(props) {super(props)this.state = store.getState()}render() {// ....
}const stateToProps = (state) => {return {inputValue: state.inputValue}
}export default connect(stateToProps, null)(TodoList);
修改 store 中的数据

React-redux 顺利的拿到 Store中 数据了。如何改变Store中的数据呢?也就是当我们修改<input>中的值时,去改变 store 数据,UI界面也随之进行改变。

TodoList.js
(绑定 onChange 事件)

<input value={this.props.inputValue} onChange={this.props.inputChange} />
inputChange(e) {console.log(e.target.value)
}
export default connect(stateToProps, null)(TodoList);

DispatchToProps就是要传递的第二个参数,通过这个参数才能改变store中的值。

const dispatchToProps = (dispatch) => {return {inputChange(e) {console.log(e.target.value)}}
}

修改 input

 <input value={this.props.inputValue} onChange={this.props.inputChange} />

把 connect 第二个参数传递过去。

export default connect(stateToProps,dispatchToProps)(TodoList);

TodoList.js
(完整代码)

import React, { Component } from 'react';
import store from './store'
import { connect } from 'react-redux'class TodoList extends Component {constructor(props) {super(props)this.state = store.getState()}render() {return (<div><div><input value={this.props.inputValue} onChange={this.props.inputChange} /><button>提交</button></div><ul><li>react redux</li></ul></div>);}
}
const stateToProps = (state) => {return {inputValue: state.inputValue}
}const dispatchToProps = (dispatch) => {return {inputChange(e) {console.log(e.target.value)}}
}export default connect(stateToProps, dispatchToProps)(TodoList);
派发 store
const dispatchToProps = (dispatch) =>{return {inputChange(e){let action = {type:'change_input',value:e.target.value}dispatch(action)}}
}

reducer.js

const defalutState = {inputValue: '请输入',list: []
}
export default (state = defalutState, action) => {if (action.type === 'change_input') {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}return state
}

不出意外,当你输入文字时,控制台会输出相对应的文字。
我们给 button 添加点击事件

<button onClick={this.props.clickButton}>提交</button>
const dispatchToProps = (dispatch) => {return {inputChange(e) {let action = {type: 'change_input',value: e.target.value}dispatch(action)},clickButton() {let action = { type: 'add_item' }dispatch(action)}}
}
const defalutState = {inputValue: '请输入',list: []
}
export default (state = defalutState, action) => {if (action.type === 'change_input') {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}if (action.type === 'add_item') {let newState = JSON.parse(JSON.stringify(state))newState.list.push(newState.inputValue)newState.inputValue = ''return newState}return state
}

映射关系

const stateToProps = (state) => {return {inputValue: state.inputValue,list: state.list}
}

界面渲染

<ul>{this.props.list.map((item, index) => {return (<li key={index}>{item}</li>)})}
</ul>

TodoList.js

import React, { Component } from 'react';
import store from './store'
import { connect } from 'react-redux'class TodoList extends Component {constructor(props) {super(props)this.state = store.getState()}render() {return (<div><div><input value={this.props.inputValue} onChange={this.props.inputChange} /><button onClick={this.props.clickButton}>提交</button></div><ul>{this.props.list.map((item, index) => {return (<li key={index}>{item}</li>)})}</ul></div>);}
}
const stateToProps = (state) => {return {inputValue: state.inputValue,list: state.list}
}const dispatchToProps = (dispatch) => {return {inputChange(e) {let action = {type: 'change_input',value: e.target.value}dispatch(action)},clickButton() {let action = { type: 'add_item' }dispatch(action)}}
}export default connect(stateToProps, dispatchToProps)(TodoList);

实现一个简单的 redux

function createStore(reducer) {let currentState;let listeners = [];function getState() {return currentState;}function subscribe(listener) {listeners.push(listener)return function unsubscribe() {const index = listeners.indexOf(listener);listeners.splice(index, 1);}}function dispatch(action) {currentState = reducer(currentState, action)for (let i = 0; i < listeners.length; i++) {const lisenter = listeners[i];lisenter()}}return {getState,subscribe,dispatch}
}

完美撒花 🎉🎉🎉🎉🎉

参考文章 https://jspang.com/detailed?id=48#toc261