这几天被redux折腾的够呛,看了很多视频,也看了很多资料。很多时候,感觉好像顿悟了,但实际上只是理解了其中的一个小概念而已。真正去做项目的时候,还是会卡壳。可能是学CSS和Javascript时花的时间太久了,学redux的时候有点浮躁。还有就是redux内容实在是不少,全部都看都理解,好像没什么必要。不看吧,用的时候总是有点力不从心。于是,决定把这些资料按自己的理解写成博客,方便自己回忆思路,也希望能帮助到需要的人

 

  redux专注于状态管理,把所有的状态都存在一个对象中。核心概念包括:store、state、action、reducer

【store】

  store是保存数据的地方,redux提供createStore函数来生成 Store。函数参数是后面要介绍的reducer

  1. import { createStore } from 'redux'
  2. const store = createStore(reducer)

【state】

  state是store的某个时刻的快照,可以通过store.getState()取得当前时刻的state

  1. const state = store.getState()

【action】

  action用来改变state。action是一个对象,其中的type属性是必须的,其他的属性一般用来设置改变state需要的数据

  1. const action = {
  2. type: 'ADD_ONE',
  3. num: 1
  4. }

  store.dispatch()是发出action的唯一方法

  1. const action = {
  2. type: 'ADD_ONE',
  3. num: 1
  4. }
  5. store.dispatch(action)

【reducer】

  reducer 是一个函数,它接受action和当前state作为参数,返回一个新的state

  1. import { createStore } from 'redux'
  2. const store = createStore(reducer)
  1. const reducer = (state = 10, action) => {
  2. switch (action.type) {
  3. case 'ADD_ONE':
  4. return state + action.num;
  5. default:
  6. return state;
  7. }
  8. };

  当store.dispatch发送过来一个新的action,store就会自动调用reducer,得到新的state

 

  多余的概念不再介绍,下面用上面介绍的这四个核心概念实现一个简单的实例,将create-react-app中index.js文件内容更改如下,即可运行

  1. //第一步,创建action
  2. const addOne = {
  3. type: 'ADD',
  4. num: 1
  5. }
  6. const addTwo = {
  7. type: 'ADD',
  8. num: 2
  9. }
  10. const square = {
  11. type: 'SQUARE'
  12. }
  13. //第二步,创建reducer
  14. let math = (state = 10, action) => {
  15. switch (action.type) {
  16. case ADD:
  17. return state + action.num
  18. case SQUARE:
  19. return state * state
  20. default:
  21. return state
  22. }
  23. }
  24. //第三步,创建store
  25. import { createStore } from 'redux'
  26. const store = createStore(math)
  27. //第四步,测试,通过dispatch发出action,并通过getState()取得当前state值
  28. console.log(store.getState()) //默认值为10
  29. store.dispatch(addOne) //发起'+1'的action
  30. console.log(store.getState()) //当前值为10+1=11
  31. store.dispatch(square) //发起'乘方'的action
  32. console.log(store.getState()) //当前值为11*11=121
  33. store.dispatch(addTwo) //发起'+2'的action
  34. console.log(store.getState()) //当前值为121+2=123

  结果如下

 

  下面对目录结构进行划分

  1、一般地,将action.type设置为常量,这样在书写错误时,会得到报错提示

  1. // constants/ActionTypes.js
  2. export const ADD = 'ADD'
  3. export const SQUARE = 'SQUARE'

  2、可以将addOne对象和addTwo对象整合成add函数的形式

  1. // action/math.js
  2. import { ADD, SQUARE } from '../constants/ActionTypes'
  3. export const add = num => ({ type: ADD, num })
  4. export const square = { type: SQUARE }

  3、根据action.type的分类来拆分reducer,最终通过combineReducers方法将拆分的reducer合并起来。上例中的action类型都是数字运算,无需拆分,只需进行如下变化

  1. // reducer/math.js
  2. import { ADD, SQUARE } from '../constants/ActionTypes'
  3. const math = (state = 10, action) => {
  4. switch (action.type) {
  5. case ADD:
  6. return state + action.num
  7. case SQUARE:
  8. return state * state
  9. default:
  10. return state
  11. }
  12. }
    export default math
  1. // reducer/index.js
  2. import { combineReducers } from 'redux'
  3. import math from './math'
  4. const rootReducer = combineReducers({
  5. math
  6. })
  7. export default rootReducer

  4、将store存储到store/index.js文件中

  1. // store/index.js
  2. import { createStore } from 'redux'
  3. import rootReducer from '../reducer'
  4. export default createStore(rootReducer)

  5、最终,根路径下的index.js内容如下所示

  1. import store from './store'
  2. import {add, square} from './action/math'
  3. console.log(store.getState()) //默认值为10
  4. store.dispatch(add(1)) //发起'+1'的action
  5. console.log(store.getState()) //当前值为10+1=11
  6. store.dispatch(square) //发起'乘方'的action
  7. console.log(store.getState()) //当前值为11*11=121
  8. store.dispatch(add(2)) //发起'+2'的action
  9. console.log(store.getState()) //当前值为121+2=123

  最终目录路径如下所示

  最终结果如下所示

 

  前面的示例中,只是redux的状态改变,下面利用UI层来建立view和state的联系,将根目录下的index.js的内容更改如下

  1. import store from './store'
  2. import React from 'react'
  3. import ReactDOM from 'react-dom'
  4. import { add, square } from './action/math'
  5. ReactDOM.render(
  6. <div store={store}>
  7. <p>{store.getState().math}</p>
  8. <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
  9. <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
  10. <input type="button" onClick={() => store.dispatch(square)} value="乘方" />
  11. </div>,
  12. document.getElementById('root')
  13. )

  虽然可以显示数字,但是点击按钮时,却不能重新渲染页面

【store.subscribe()】

  接下来介绍store.subscribe()方法了,该方法用来设置监听函数,一旦state发生变化,就自动执行这个函数。该方法的返回值是一个函数,调用这个函数可以解除监听

  下面将示例代码更改如下

  1. import store from './store'
  2. import React from 'react'
  3. import ReactDOM from 'react-dom'
  4. import { add, square } from './action/math'
  5.  
  6. const render = () => ReactDOM.render(
  7. <div store={store}>
  8. <p>{store.getState().math}</p>
  9. <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
  10. <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
  11. <input type="button" onClick={() => store.dispatch(square)} value="乘方" />
  12. </div>,
  13. document.getElementById('root')
  14. )
  15. render()
  16. store.subscribe(render)

  代码终于可以正常运行了

 

  redux默认只处理同步,对于API请求这样的异步任务则无能为力

  接下来尝试使用axios的get方法来请求下面这个API

  1. https://jsonplaceholder.typicode.com/posts/2

  获取的数据如下

  1. {
  2. "userId": 1,
  3. "id": 2,
  4. "title": "qui est esse",
  5. "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  6. }

  然后,将其id值设置为state.math的值

  代码修改如下

  1. // constants/ActionTypes.js
  2. export const ADD = 'ADD'
  3. export const SQUARE = 'SQUARE'
  4. export const SET = 'SET'
  5.  
  6. // action/math.js
  7. import { ADD, SQUARE, SET } from '../constants/ActionTypes'
  8. export const add = num => ({ type: ADD, num })
  9. export const square = { type: SQUARE }
  10. export const setNum = num => ({type: SET,num})
  11. // reduce/math.js
  12. import { ADD, SQUARE,SET } from '../constants/ActionTypes'
  13. const math = (state = 10, action) => {
  14. switch (action.type) {
  15. case ADD:
  16. return state + action.num
  17. case SQUARE:
  18. return state * state
  19. case SET:
  20. return action.num
  21. default:
  22. return state
  23. }
  24. }
  25. export default math
  26. // index.js
  27. import store from './store'
  28. import React from 'react'
  29. import ReactDOM from 'react-dom'
  30. import { add, square, setNum } from './action/math'
  31. import axios from 'axios'
  32. let uri = 'https://jsonplaceholder.typicode.com/posts/2'
  33. const render = () => ReactDOM.render(
  34. <div store={store}>
  35. <p>{store.getState().math}</p>
  36. <input type="button" onClick={() => {axios.get(uri).then(res => {store.dispatch(store.dispatch(setNum(res.data.id)))})}} value="设置Num" />
  37. <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
  38. <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
  39. <input type="button" onClick={() => store.dispatch(square)} value="乘方" />
  40. </div>,
  41. document.getElementById('root')
  42. )
  43. render()
  44. store.subscribe(render)

  效果如下

  但是,虽然API是异步操作,但store.dispatch并不是异步,而axios通过get方法请求回来数据后,store.dispatch在axios中的then方法中同步取得数据

【redux-thunk】

  如果要使用真正的异步操作,即把axios方法封装到store.dispatch中,需要使用redux-thunk中间件

  首先,使用npm进行安装

  1. npm install --save redux-thunk

  然后,使用applyMiddleware来使用thunk中间件

  1. import { createStore, applyMiddleware } from 'redux'
  2. import thunk from 'redux-thunk'
  3. import rootReducer from '../reducer'
  4. export default createStore(rootReducer,applyMiddleware(thunk))

  接着来定义setNum这个action creator,然后在index.js文件的DOM加载完成后就发出setNum

  [注意]如果action是一个对象,则它就是一个action,如果action是一个函数,则它是一个action creator,即action制造器

  修改的代码如下

  1. // action/math.js
  2. import { ADD, SQUARE, SET } from '../constants/ActionTypes'
  3. import axios from 'axios'
  4. const uri = 'https://jsonplaceholder.typicode.com/posts/2'
  5. export const add = num => ({ type: ADD, num })
  6. export const square = { type: SQUARE }
  7. export const setNum = () => (dispatch, getState) => {
  8. return axios.get(uri).then(res => {
  9. dispatch({
  10. type: SET,
  11. num: res.data.id
  12. })
  13. })
  14. }
  15. // index.js
  16. import store from './store'
  17. import React from 'react'
  18. import ReactDOM from 'react-dom'
  19. import { add, square, setNum } from './action/math'
  20. const render = () => ReactDOM.render(
  21. <div store={store}>
  22. <p>{store.getState().math}</p>
  23. <input type="button" onClick={() => store.dispatch(setNum())} value="设置Num" />
  24. <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
  25. <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
  26. <input type="button" onClick={() => store.dispatch(square)} value="乘方" />
  27. </div>,
  28. document.getElementById('root')
  29. )
  30. render()
  31. store.subscribe(render)

  效果如下

【提示信息】

  如果做的更完备一点,应该把异步请求时的提示信息也加上。增加一个fetch的action,用于控制fetch过程的提示信息及显示隐藏情况

  代码更改如下

  1. // action/fetch.js
  2. import { SET_FETCH_MESSAGE, HIDE_FETCH_MESSAGE } from '../constants/ActionTypes'
  3. export const startFetch = { type: SET_FETCH_MESSAGE,message: '开始发送异步请求' }
  4. export const successFetch = { type: SET_FETCH_MESSAGE, message: '成功接收数据' }
  5. export const failFetch = { type: SET_FETCH_MESSAGE, message: '接收数据失败' }
  6. export const hideFetchMessage = { type: HIDE_FETCH_MESSAGE }
  1. // action/math.js
  2. import { ADD, SQUARE, SET } from '../constants/ActionTypes'
  3. import { startFetch, successFetch, failFetch, hideFetchMessage } from './fetch'
  4. import axios from 'axios'
  5. const uri = 'https://jsonplaceholder.typicode.com/posts/2'
  6. export const add = num => ({ type: ADD, num })
  7. export const square = { type: SQUARE }
  8. export const setNum = () => (dispatch, getState) => {
  9. dispatch(startFetch)
  10. setTimeout(() => {
  11. dispatch(hideFetchMessage)
  12. }, 500)
  13. return axios
  14. .get(uri)
  15. .then(res => {
  16. setTimeout(() => {
  17. dispatch(successFetch)
  18. setTimeout(() => {
  19. dispatch(hideFetchMessage)
  20. }, 500)
  21. dispatch({ type: SET, num: res.data.id })
  22. }, 1000)
  23. })
  24. .catch(err => {
  25. dispatch(failFetch)
  26. setTimeout(() => {
  27. dispatch(hideFetchMessage)
  28. }, 500)
  29. })
  30. }
  1. // constants/ActionTypes.js
  2. export const ADD = 'ADD'
  3. export const SQUARE = 'SQUARE'
  4. export const SET = 'SET'
  5. export const SET_FETCH_MESSAGE = 'SET_FETCH_MESSAGE'
  6. export const HIDE_FETCH_MESSAGE = 'HIDE_FETCH_MESSAGE'
  1. // reduce/fetch.js
  2. import { SET_FETCH_MESSAGE,HIDE_FETCH_MESSAGE } from '../constants/ActionTypes'
  3. const initState = {
  4. message:'',
  5. isShow:false
  6. }
  7. const fetch = (state = initState, action) => {
  8. switch (action.type) {
  9. case SET_FETCH_MESSAGE:
  10. return {isShow: true, message: action.message}
  11. case HIDE_FETCH_MESSAGE:
  12. return { isShow: false, message: '' }
  13. default:
  14. return state
  15. }
  16. }
  17. export default fetch
  1. // index.js
  2. import store from './store'
  3. import React from 'react'
  4. import ReactDOM from 'react-dom'
  5. import { add, square, setNum } from './action/math'
  6. const render = () => ReactDOM.render(
  7. <div store={store}>
  8. <p>{store.getState().math}</p>
  9. <input type="button" onClick={() => store.dispatch(setNum())} value="设置Num" />
  10. <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
  11. <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
  12. <input type="button" onClick={() => store.dispatch(square)} value="乘方" />
  13. {store.getState().fetch.isShow && <p>{store.getState().fetch.message}</p>}
  14. </div>,
  15. document.getElementById('root')
  16. )
  17. render()
  18. store.subscribe(render)

  效果如下

  

  下面来介绍react-redux。前面的代码中,我们是通过store.subscribe()方法监控state状态的变化来更新UI层的。而使用react-redux,可以让组件动态订阅状态树。状态树一旦被修改,组件能自动刷新显示最新数据

  react-redux将所有组件分成两大类:展示组件和容器组件。展示组件只负责UI呈现,所有数据由参数props提供;容器组件则负责管理数据和业务逻辑,带有内部状态,可使用redux的API。要使用react-redux,就要遵守它的组件拆分规范

【provider】

  react-redux提供Provider组件,可以让容器组件默认可以拿到state,而不用当容器组件层级很深时,一级级将state传下去

  将index.js文件更改如下

  1. // index.js
  2. import React from 'react'
  3. import ReactDOM from 'react-dom'
  4. import store from './store'
  5. import MathContainer from './container/MathContainer'
  6. import { Provider } from 'react-redux'
  7. ReactDOM.render(
  8. <Provider store={store}>
  9. <MathContainer />
  10. </Provider>,
  11. document.getElementById('root')
  12. )

  按照组件拆分规范,将原来index.js中相关代码,分拆到container/MathContainer和component/Math这两个组件中

【connect】

  react-redux提供connect方法,用于从展示组件生成容器组件。connect的意思就是将这两种组件连接起来

  1. import { connect } from 'react-redux'
  2. const MathContainer = connect()(Math);

  Math是展示组件,MathContainer就是由React-redux通过connect方法自动生成的容器组件

  为了定义业务逻辑,需要给出下面两方面的信息

  1、输入逻辑:外部的数据(即state对象)如何转换为展示组件的参数

  2、输出逻辑:用户发出的动作如何变为Action对象,从展示组件传出去

  因此,connect方法的完整API如下

  1. import {connect} from 'react-redux'
  2. const MathContainer= connect(
  3. mapStateToProps,
  4. mapDispatchToProps
  5. )(Math)

  上面代码中,connect方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了展示组件的业务逻辑。前者负责输入逻辑,即将state映射到UI组件的参数(props),后者负责输出逻辑,即将用户对展示组件的操作映射成Action

【mapStateToProps()】

  mapStateToProps建立一个从外部的state对象到展示组件的props对象的映射关系。作为参数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。

  1. const mapStateToProps = (state) => {
  2. return {
  3. num: getNum(state)
  4. }
  5. }

  mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发展示组件重新渲染

  1. const mapStateToProps = (state,ownProps) => {
  2. return {
  3. num: getNum(state)
  4. }
  5. }

  mapStateToProps会订阅Store,每当state更新的时候,就会自动执行,重新计算展示组件的参数,从而触发展示组件的重新渲染。connect方法可以省略mapStateToProps参数,那样,展示组件就不会订阅Store,就是说Store的更新不会引起展示组件的更新

【mapDispatchToProps】

  mapDispatchToProps是connect函数的第二个参数,用来建立展示组件的参数到store.dispatch方法的映射。也就是说,它定义了用户的哪些操作应该当作action,传给Store。它可以是一个函数,也可以是一个对象

  如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数

  1. const mapDispatchToProps = (dispatch,ownProps) => {
  2. return {
  3. onSetNumClick: () => dispatch(setNum())
  4. }
  5. }

  mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了展示组件的参数怎样发出action

  如果mapDispatchToProps是一个对象,它的每个键名也是对应展示组件的同名参数,键值应该是一个函数,会被当作action creator,返回的action会由redux自动发出

  因此,上面的写法简写如下所示

  1. const mapDispatchToProps = {
  2. onsetNumClick: () => setNum()
  3. }

 

  由于store目录中,只能一个index.js文件,且不会有内容扩展,将其更改为根目录下的store.js文件

  将其他的目录都变成复数形式,最终的目录结构如下所示

  效果如下

  详细代码如下所示,且可访问github线上地址

【components】

  1. // components/Math.js
  2. import React from 'react'
  3. const Math = ({
  4. num,
  5. isShow,
  6. fetchMessage,
  7. onSetNumClick,
  8. onAddOneClick,
  9. onAddTwoClick,
  10. onSqureClick
  11. }) => (
  12. <section>
  13. <p>{num}</p>
  14. <input type="button" onClick={onSetNumClick} value="设置Num" />
  15. <input type="button" onClick={onAddOneClick} value="+1" />
  16. <input type="button" onClick={onAddTwoClick} value="+2" />
  17. <input type="button" onClick={onSqureClick} value="乘方" />
  18. {isShow && <p>{fetchMessage}</p>}
  19. </section>
  20. )
  21. export default Math

【containers】

  1. // containers/MathContainer.js
  2. import { connect } from 'react-redux'
  3. import Math from '../components/Math'
  4. import { getNum } from '../selectors/math'
  5. import { getFetchMessage, getFetchIsShow } from '../selectors/fetch'
  6. import { setNum, add, square } from '../actions/math'
  7. const mapStateToProps = state => {
  8. return {
  9. num: getNum(state),
  10. fetchMessage: getFetchMessage(state),
  11. isShow: getFetchIsShow(state)
  12. }
  13. }
  14. const mapDispatchToProps = {
  15. onSetNumClick: () => setNum(),
  16. onAddOneClick: () => add(1),
  17. onAddTwoClick: () => add(2),
  18. onSqureClick: () => square
  19. }
  20. const MathContainer = connect(mapStateToProps, mapDispatchToProps)(Math)
  21. export default MathContainer

【actions】

  1. // actions/fetch.js
  2. import { SET_FETCH_MESSAGE, HIDE_FETCH_MESSAGE } from '../constants/ActionTypes'
  3. export const startFetch = { type: SET_FETCH_MESSAGE,message: '开始发送异步请求' }
  4. export const successFetch = { type: SET_FETCH_MESSAGE, message: '成功接收数据' }
  5. export const failFetch = { type: SET_FETCH_MESSAGE, message: '接收数据失败' }
  6. export const hideFetchMessage = { type: HIDE_FETCH_MESSAGE }
  1. // actions/math.js
  2. import { ADD, SQUARE, SET } from '../constants/ActionTypes'
  3. import { startFetch, successFetch, failFetch, hideFetchMessage } from './fetch'
  4. import axios from 'axios'
  5. const uri = 'https://jsonplaceholder.typicode.com/posts/2'
  6. export const add = num => ({ type: ADD, num })
  7. export const square = { type: SQUARE }
  8. export const setNum = () => (dispatch, getState) => {
  9. dispatch(startFetch)
  10. setTimeout(() => {dispatch(hideFetchMessage)}, 300)
  11. return axios
  12. .get(uri)
  13. .then(res => {
  14. dispatch(successFetch)
  15. setTimeout(() => {dispatch(hideFetchMessage)}, 300)
  16. dispatch({ type: SET, num: res.data.id })
  17. })
  18. .catch(err => {
  19. dispatch(failFetch)
  20. setTimeout(() => {dispatch(hideFetchMessage)}, 300)
  21. })
  22. }

【constants】

  1. // constants/ActionTypes.js
  2. export const ADD = 'ADD'
  3. export const SQUARE = 'SQUARE'
  4. export const SET = 'SET'
  5. export const SET_FETCH_MESSAGE = 'SET_FETCH_MESSAGE'
  6. export const HIDE_FETCH_MESSAGE = 'HIDE_FETCH_MESSAGE'

【reducers】

  1. // reducers/fetch.js
  2. import { SET_FETCH_MESSAGE,HIDE_FETCH_MESSAGE } from '../constants/ActionTypes'
  3. const initState = {
  4. message:'',
  5. isShow:false
  6. }
  7. const fetch = (state = initState, action) => {
  8. switch (action.type) {
  9. case SET_FETCH_MESSAGE:
  10. return {isShow: true, message: action.message}
  11. case HIDE_FETCH_MESSAGE:
  12. return { isShow: false, message: '' }
  13. default:
  14. return state
  15. }
  16. }
  17. export default fetch
  1. // reducers/index.js
  2. import { combineReducers } from 'redux'
  3. import math from './math'
  4. import fetch from './fetch'
  5. const rootReducer = combineReducers({
  6. math,
  7. fetch
  8. })
  9. export default rootReducer
  1. // reduces/math.js
  2. import { ADD, SQUARE,SET } from '../constants/ActionTypes'
  3. const math = (state = 10, action) => {
  4. switch (action.type) {
  5. case ADD:
  6. return state + action.num
  7. case SQUARE:
  8. return state * state
  9. case SET:
  10. return action.num
  11. default:
  12. return state
  13. }
  14. }
  15. export default math

【selectors】

  1. // selectors/fetch.js
  2. export const getFetchMessage = state => state.fetch.message
  3. export const getFetchIsShow = state => state.fetch.isShow
  1. // selectors/math.js
  2. export const getNum = state => state.math

【根目录】

  1. // index.js
  2. import React from 'react'
  3. import ReactDOM from 'react-dom'
  4. import store from './store'
  5. import MathContainer from './containers/MathContainer'
  6. import { Provider } from 'react-redux'
  7. ReactDOM.render(
  8. <Provider store={store}>
  9. <MathContainer />
  10. </Provider>,
  11. document.getElementById('root')
  12. )
  1. // store.js
  2. import { createStore, applyMiddleware } from 'redux'
  3. import thunk from 'redux-thunk'
  4. import rootReducer from './reducers'
  5. export default createStore(rootReducer,applyMiddleware(thunk))

 

版权声明:本文为xiaohuochai原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/xiaohuochai/p/8447826.html