Redux 学习笔记

375 阅读9分钟

Redux 学习笔记

Redux 简介

Redux 的作者为 Facebook 的 Dan Abramov 大神。Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

Redux 并不是在项目中一定要用到的东西,相反,只有遇到 React 实在解决不了的问题你才需要用 Redux。

一般来说下面几种情况可能会用到 Redux:

  • 用户的使用方式非常复杂
  • 不同身份的用户有不同的使用方式(比如普通用户和管理员)
  • 多个用户之间可以协作
  • 与服务器大量交互,或者使用了 WebSocket
  • View 要从多个来源获取数据

从组件的角度看,下面几种可以考虑使用 Redux:

  • 某个组件的状态,需要共享
  • 某个状态需要在任何地方都可以拿到
  • 一个组件需要改变全局状态
  • 一个组件需要改变另一个组件的状态

Redux 的特性

单一数据源

单一数据源(Single Source of Truth) 就是整个应用程序只存在于一个唯一的 store 中。

可预测性

改变 state 唯一的方法就是触发 action,可以用一个公式来表达就是 state + action = new state

纯函数更新 Store

为了描述 action 如何更新 state,你需要编写 reducer,而 reducer 必须是纯函数,输出结果完全取决于它的输入参数,函数内部不依赖任何的外部依赖和外部资源。

Action

action 描述的一个行为的数据结构,它是 store 的唯一数据来源。

一般来说,action 就是 JavaScript 中的一个普通的对象,一般会有一个 type 属性来表示要执行的动作。

const ADD_TODO = 'ADD_TODO'
const action = {
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Action是view发出的通知,表示state应该要发生变化了。

Reducer

接收到 action 之后,进行相应的响应,以更新 state。

store 收到 Action 以后,必须给出一个新的 State,View 才会变化,这种 State 的计算过程就叫 Reducer。

Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

Reducer 可以定义多个,每一个 Reducer 都可以接收到所有的 Action 。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return { ...state, ...{ stateKey: action.newState } }
    default:
      return state
  }
}

Store

Store 包含三个部分:State、Reducer、Dispatcher。

State 是真正的数据;Reducer 是处理 action 的,用来更新 State 的;Dispatcher 是用来派发 action 的。

View 上的一些操作会触发 Action,Action 传给 Dispatcher 去进行派发,给到 Reducer,Reducer 对 State 进行更新,State 更新之后就会在 View 上得到体现。整个过程是一个单向数据流。

核心 API

createStore(reducer)

创建 Store,需要传入一个 reducer 作为参数。

import { createStore } from 'redux'
import reducers from './reducers'
const store = createStore(reducers)

getState()

获取当前的 State 值。

store.getState()

dispatch(action)

派发 action,需要传入一个 action 作为参数,我们想要执行某些操作的时候就去 dispatch 相应的 action 就行。

// action creator
function action1() {
  return { type: 'ACTION1', newState: Math.random() }
}

function action3() {
  return { type: 'ACTION3', newState: Math.random() }
}

// store.dispatch({ type: 'ACTION1', newState: Math.random() })
// store.dispatch({ type: 'ACTION3', newState: Math.random() })
store.dispatch(action1())
store.dispatch(action3())

subscribe(listener)

订阅 store ,state 变化的时候会触发 执行 listener。

store.subscribe(() => console.log(store.getState()))

combineReducers({ reducer1, reducer2, ... })

把多个 Reducer 连接在一起形成一个新的 Reducer。

reducer1.js

export default function reducer1(state = { state1: 1, state2: 'abc' }, action) {
  switch (action.type) {
    case 'ACTION1':
      return { ...state, ...{ state1: action.newState } }
    case 'ACTION2':
      return { ...state, ...{ state2: action.newState } }
    default:
      return state
  }
}

reducer2.js

export default function reducer2(state = { state3: 3, state4: 'def' }, action) {
  switch (action.type) {
    case 'ACTION3':
      return { ...state, ...{ state3: action.newState } }
    case 'ACTION4':
      return { ...state, ...{ state4: action.newState } }
    default:
      return state
  }
}

reducer.js

import { combineReducers } from 'redux'
import reducer1 from './reducer1'
import reducer2 from './reducer2'

export default combineReducers({
  reducer1,
  reducer2,
})

bindActionCreators

此函数是为了简化 Action Creator 和 dispatch 函数使用的流程。

import { bindActionCreators, createStore } from 'redux'

// 前面的例子
// action creator
function action1() {
  return { type: 'ACTION1', newState: Math.random() }
}

store.dispatch(action1())

// 等同于如下写法
function action1() {
  return { type: 'ACTION1', newState: Math.random() }
}
// action 参数可以是一个对象,并可以传入多个 action creator
const action1Dispatch = bindActionCreators(action1, store.dispatch)
action1Dispatch()

connect

在 React 组件中想要使用 Redux,必须使用 connect 把,Redux 与 React 组件连接起来。connect 本质是一个高阶函数。

connect(mapStateToProps, mapDispatchToProps)(reactComponent): newReactComponent

传入本组件需要的 state 和 dispatch 函数,返回一个新的 React 组件。

import React from 'react'
import { connect, Provider } from 'react-redux'
...

class MyComponent1 extends React.Component {
  // ...
}

// 此处要对 Store 中的 state 进行筛选
// 如果不筛选那么 Store 中的任何 state 更新都会导致组件刷新
// 这样会导致额外的性能开销
function mapStateToProps(state) {
  return {
    key: state.key,
    ...
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({ actionCreator1, actionCreator2, ... }, dispatch)
  }
}

const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)

class MyComponent1WithStore extends React.Component {
  render() {
    return (
      // Provider 下的组件都可以访问到 store
      <Provider store={store}>
        <ConnectedMyComponent1 />
      </Provider>
    )
  }
}

Middleware 与异步操作

中间件就是截获某种类型的 action,比如对 ajax 请求,会进行一个预处理,然后形成最终的 action,发出真正的 action。其过程如下图所示:

中间件使用

使用 applyMiddlewares() 来将所有中间件组成一个数组,一次执行。

以 redux-logger 这个中间件为例。

import { applyMiddleware, createStore } from 'redux'
import { createLogger } from 'redux-logger'
const logger = createLogger()

const store = createStore(
  reducer,
  applyMiddleware(logger)
)

异步

同步操作只要发出一种 Action 即可,异步操作要发出三种 Action(开始、成功、失败)。

下面代码片段引入了异步操作,修改了 reducer 对 action 的处理,在异步操作中去 dispatch action。

function reducers(state = { count: 0, state: 'init' }, action) {
  switch (action.type) {
    case 'PLUS_START':
      return { count: state.count, state: 'pending' }
    case 'PLUS_SUCCESS':
      return { count: state.count + 2, state: 'plus success' }
    case 'PLUS_FAILED':
      return { count: state.count, state: 'failed' }
	...
    default:
      return state
  }
}

function plusFailed() {
  stop(1000, false).then(res => {
    store.dispatch({ type: 'PLUS_SUCCESS' })
  }).catch(e => {
    store.dispatch({ type: 'PLUS_FAILED' })
  })
  return { type: 'PLUS_START' }
}
...

异步中间件

redux-thunk

上面是异步的基本用法,但是我们需要在异步请求的前后多次去调用 dispatch,这显得并不是那么方便。如果有一个办法可以直接在 dispatch 里面放一个异步操作的函数就会方便很多了,不过 dispatch 却只能将形如{ type: 'PLUS_SUCCESS' }的对象作为参数传进来。

想要赋予 dispatch 能够接收一个函数作为参数的能力那么就需要用到 redux-thunk 中间件了。

引入方法如下所示:

import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
 
const store = createStore(
  reducers, 
  applyMiddleware(thunk)
)

使用方法:

function asyncFunction(param) {
  return dispatch => {
    stop(1000, param).then(res => {
      dispatch({ type: 'PLUS_SUCCESS' })
    })
  }
}

// 使用方法一
store.dispatch(asyncFunction(param))
// 使用方法二
store.dispatch(asyncFunction(param)).then(() =>
  console.log(store.getState())
)

redux-promise

这个中间件可以让 dispatch 具备接收 promise 对象的能力。

引入方法如下所示:

import { createStore, applyMiddleware } from 'redux'
import promiseMiddleware from 'redux-promise'
import reducer from './reducers'

const store = createStore(
  reducer,
  applyMiddleware(promiseMiddleware)
)

使用方法:

// 如果 reject 将不会 dispatch action 
function plusPromise() {
  return new Promise((resolve, reject) => {
    stop(1000, true).then(() => {
      resolve({ type: 'PLUS_SUCCESS' })
    })
  })
}

store.dispatch(plusPromise())

Hooks

hook 版本简化了在组件中使用 redux,mapStateToProps、mapDispatchToProps、connect 还有中间件都不需要了。

子组件中调用 action:

  • useSelector 替代 mapStateToProps
  • useDispatch 替代 mapDispatchToProps,且可以不使用中间件直接进行异步操作

引入方法:

import { useSelector, useDispatch } from 'react-redux'

直接在函数组件中就可以使用,使用方法:

const count = useSelector(state => {
  console.log(state)
  return state.count
})

function plus() {
  dispatch({ type: 'PLUS_SUCCESS' })
}

完整 Demo

pure-redux

reducer1.js、 reducer2.js 和 reducer.js 同 combineReducers 的例子。

index.js 如下:

import React from 'react'
import ReactDOM from 'react-dom'
import { bindActionCreators, createStore } from 'redux'
import reducers from './reducers'

function click() {
  const store = createStore(reducers)

  function action1() {
    return { type: 'ACTION1', newState: Math.random() }
  }
  
  function action3() {
    return { type: 'ACTION3', newState: Math.random() }
  }

  store.subscribe(() => console.log(store.getState()))
  // store.dispatch({ type: 'ACTION1', newState: Math.random() })
  // store.dispatch({ type: 'ACTION3', newState: Math.random() })

  const action1Dispatch = bindActionCreators(action1, store.dispatch)
  action1Dispatch()
  store.dispatch(action3())
}

function Button() {
  return (
    <button onClick={click}>click me</button>
  )
}

ReactDOM.render(
  <Button />,
  document.getElementById('root')
)

Redux 与 React 组件结合使用

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore } from 'redux'

function reducers(state = { count: 0 }, action) {
  switch (action.type) {
    case 'PLUS_ONE':
      return { count: state.count + 1 }
    case 'MINUS_ONE':
      return { count: state.count - 1 }
    default:
      return state
  }
}

const store = createStore(reducers)

function plusOne() {
  return { type: 'PLUS_ONE' }
}

function minusOne() {
  return { type: 'MINUS_ONE' }
}

class MyComponent1 extends React.Component {
  render() {
    const { count } = this.props
    const { minusOne, plusOne } = this.props.actions
    return (
      <div>
        <button onClick={minusOne}>-</button>
          {count}
        <button onClick={plusOne}>+</button>
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    count: state.count,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({ plusOne, minusOne }, dispatch)
  }
}

const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)

class MyComponent1WithStore extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <ConnectedMyComponent1 />
      </Provider>
    )
  }
}

ReactDOM.render(
  <MyComponent1WithStore />,
  document.getElementById('root')
)

中间件

import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'

const logger = createLogger();

const store = createStore(
  reducers,
  applyMiddleware(logger)
)

function reducers(state = { count: 0 }, action) {
  switch (action.type) {
    case 'PLUS_ONE':
      return { count: state.count + 1 }
    case 'MINUS_ONE':
      return { count: state.count - 1 }
    default:
      return state
  }
}

function plusOne() {
  return { type: 'PLUS_ONE' }
}

function minusOne() {
  return { type: 'MINUS_ONE' }
}

class MyComponent1 extends React.Component {
  render() {
    const { count } = this.props
    const { minusOne, plusOne } = this.props.actions
    return (
      <div>
        <button onClick={minusOne}>-</button>
          {count}
        <button onClick={plusOne}>+</button>
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    count: state.count,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({ plusOne, minusOne }, dispatch)
  }
}

const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)

class MyComponent1WithStore extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <ConnectedMyComponent1 />
      </Provider>
    )
  }
}

ReactDOM.render(
  <MyComponent1WithStore />,
  document.getElementById('root')
)

异步操作

import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'

function stop(ms, isSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (isSuccess) {
        resolve('success')
      } else {
        reject('failed')
      }
    }, ms)
  })  
}

const logger = createLogger();

const store = createStore(
  reducers,
  applyMiddleware(logger)
)

function reducers(state = { count: 0, state: 'init' }, action) {
  switch (action.type) {
    case 'PLUS_START':
      return { count: state.count, state: 'pending' }
    case 'PLUS_SUCCESS':
      return { count: state.count + 2, state: 'plus success' }
    case 'PLUS_FAILED':
      return { count: state.count, state: 'failed' }
    case 'MINUS_START':
      return { count: state.count, state: 'pending' }
    case 'MINUS_SUCCESS':
      return { count: state.count - 2, state: 'minus success' }
    case 'MINUS_FAILED':
      return { count: state.count, state: 'failed' }
    default:
      return state
  }
}

function plusFailed() {
  stop(1000, false).then(res => {
    store.dispatch({ type: 'PLUS_SUCCESS' })
  }).catch(e => {
    store.dispatch({ type: 'PLUS_FAILED' })
  })
  return { type: 'PLUS_START' }
}

function minusFailed() {
  stop(1000, false).then(res => {
    store.dispatch({ type: 'MINUS_SUCCESS' })
  }).catch(e => {
    store.dispatch({ type: 'MINUS_FAILED' })
  })
  return { type: 'MINUS_START' }
}

function plusSuccess() {
  stop(1000, true).then(res => {
    store.dispatch({ type: 'PLUS_SUCCESS' })
  }).catch(e => {
    store.dispatch({ type: 'PLUS_FAILED' })
  })
  return { type: 'PLUS_START' }
}

function minusSuccess() {
  stop(1000, true).then(res => {
    store.dispatch({ type: 'MINUS_SUCCESS' })
  }).catch(e => {
    store.dispatch({ type: 'MINUS_FAILED' })
  })
  return { type: 'MINUS_START' }
}

class MyComponent1 extends React.Component {
  render() {
    const { count, state } = this.props
    const { minusSuccess, plusSuccess, minusFailed, plusFailed } = this.props.actions
    return (
      <div>
        <button onClick={minusSuccess}> - 成功</button>
        <button onClick={minusFailed}> - 失败</button>
          {count}
        <button onClick={plusSuccess}> + 成功</button>
        <button onClick={plusFailed}> + 失败</button>
        <p>
          {state}
        </p>
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    count: state.count,
    state: state.state,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({ plusSuccess, minusSuccess, plusFailed, minusFailed }, dispatch)
  }
}

const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)

class MyComponent1WithStore extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <ConnectedMyComponent1 />
      </Provider>
    )
  }
}

ReactDOM.render(
  <MyComponent1WithStore />,
  document.getElementById('root')
)

redux-thunk

import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk';

function stop(ms, isSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (isSuccess) {
        resolve('success')
      } else {
        reject('failed')
      }
    }, ms)
  })  
}

const logger = createLogger();

const store = createStore(
  reducers,
  applyMiddleware(thunk, logger)
)

function reducers(state = { count: 0, state: 'init' }, action) {
  switch (action.type) {
    case 'PLUS_SUCCESS':
      return { count: state.count + 2, state: 'plus success' }
    case 'MINUS_SUCCESS':
      return { count: state.count - 2, state: 'minus success' }
    default:
      return state
  }
}

function plusAsync() {
  return dispatch => {
    stop(1000, true).then(res => {
      dispatch({ type: 'PLUS_SUCCESS' })
    })
  }
}

function minusAsync() {
  return dispatch => {
    stop(1000, true).then(res => {
      dispatch({ type: 'MINUS_SUCCESS' })
    })
  }
}

class MyComponent1 extends React.Component {
  render() {
    const { count, state } = this.props
    const { plusAsync, minusAsync } = this.props.actions
    return (
      <div>
        <button onClick={minusAsync}>-</button>
          {count}
        <button onClick={plusAsync}>+</button>
        <p>
          {state}
        </p>
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    count: state.count,
    state: state.state,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({ plusAsync, minusAsync }, dispatch)
  }
}

const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)

class MyComponent1WithStore extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <ConnectedMyComponent1 />
      </Provider>
    )
  }
}

ReactDOM.render(
  <MyComponent1WithStore />,
  document.getElementById('root')
)

redux-promise

import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import promiseMiddleware from 'redux-promise'

function stop(ms, isSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (isSuccess) {
        resolve('success')
      } else {
        reject('failed')
      }
    }, ms)
  })  
}

const logger = createLogger();

const store = createStore(
  reducers,
  applyMiddleware(promiseMiddleware, logger)
)

function reducers(state = { count: 0, state: 'init' }, action) {
  switch (action.type) {
    case 'PLUS_SUCCESS':
      return { count: state.count + 2, state: 'plus success' }
    case 'MINUS_SUCCESS':
      return { count: state.count - 2, state: 'minus success' }
    default:
      return state
  }
}

// 如果 reject 将不会 dispatch action 
function plusPromise() {
  return new Promise((resolve, reject) => {
    stop(1000, true).then(() => {
      resolve({ type: 'PLUS_SUCCESS' })
    })
  })
}

function minusPromise() {
  return new Promise((resolve, reject) => {
    stop(1000, true).then(() => {
      resolve({ type: 'MINUS_SUCCESS' })
    })
  })
}

class MyComponent1 extends React.Component {
  render() {
    const { count, state } = this.props
    const { plusPromise, minusPromise } = this.props.actions
    return (
      <div>
        <button onClick={minusPromise}>-</button>
          {count}
        <button onClick={plusPromise}>+</button>
        <p>
          {state}
        </p>
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    count: state.count,
    state: state.state,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({ plusPromise, minusPromise }, dispatch)
  }
}

const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)

class MyComponent1WithStore extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <ConnectedMyComponent1 />
      </Provider>
    )
  }
}

ReactDOM.render(
  <MyComponent1WithStore />,
  document.getElementById('root')
)

Hooks

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import { useSelector, useDispatch } from 'react-redux'

function stop(ms, isSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (isSuccess) {
        resolve('success')
      } else {
        reject('failed')
      }
    }, ms)
  })  
}

const logger = createLogger();

const store = createStore(
  reducers,
  applyMiddleware(logger)
)

function reducers(state = { count: 0, state: 'init' }, action) {
  switch (action.type) {
    case 'PLUS_SUCCESS':
      return { count: state.count + 2, state: 'plus success' }
    case 'MINUS_SUCCESS':
      return { count: state.count - 2, state: 'minus success' }
    default:
      return state
  }
}

function MyComponent1() {
  const { count, state } = useSelector(state => {
    console.log(state)
    return state
  });
  const dispatch = useDispatch();
  
  function plus() {
    stop(1000, true).then(() => {
      dispatch({ type: 'PLUS_SUCCESS' })
    })
  }

  function minus() {
    dispatch({ type: 'MINUS_SUCCESS' })
  }

  return (
    <div>
      <button onClick={minus}>-</button>
        {count}
      <button onClick={plus}>+</button>
      <p>
        {state}
      </p>
    </div>
  )
}

ReactDOM.render(
  <Provider store={store}>
    <MyComponent1 />
  </Provider>,
  document.getElementById('root')
)