Redux状态管理

Redux

资料

  • react-redux流程与实现分析
  • 《看漫画,学 Redux》 —— A cartoon intro to Redux
  • Redux 中文网
  • React 实践心得:react-redux 之 connect 方法详解
  • 解读redux工作原理

React 是构建 UI 的库,只是 DOM 的一个抽象层,并不是 Web 应用的完整解决方案。

有两个方面,它没涉及
1 代码结构
2 组件之间的通信

对于大型的复杂应用来说,这两方面恰恰是最关键的。因此,只用 React 没法写大型应用。

Redux状态管理_第1张图片

Redux 概述

Redux 是 JavaScript 应用的可预测状态容器,用来集中管理状态。(A predictable state container for Javascript apps)

特点:集中管理、可预测、易于测试、易于调试、强大的中间件机制满足你所有需求。

注意:redux 是一个独立于 react 的库,可以配合任何 UI 库/框架来使用。

注意:如果你还不知道是否需要使用 Redux,那就是不需要它。

yarn add redux

三个核心概念

  1. action 动作,用来描述要执行的动作
    • 比如:计数器案例中的 +1 就是一个动作
    • 比如:登录功能中,登录就是一个动作
    • 可以比喻成“砖” 家,仅仅是提出想法
  2. reducer 接收到 action ,并且来执行这个动作
    • 实际的执行者,可以看做是 “搬砖的”
  3. store 是 action 和 reducer 的桥梁,将 action 传递给 reducer ,由 reducer 来完成整个动作
    • 比喻成 “管理则”

整个过程: 管理者(store) 将 专家提出来的想法(action) 传递给 搬砖的人(reducer),最终,由reducer完成了这个任务(动作)。

action

action 用来描述要执行的动作(功能)

action 实际上就是普通的JS对象

约定1:必须提供 type 属性,type 属性用来描述当前动作的类型

约定2:type 属性的值是一个字符串,任务类型使用纯大写字母来表示,多个单词之间使用 _ 分割

约定3:可以携带完成该动作需要的其他数据,这些数据(属性名)是可以任意名称的

// action 

// 添加任务的动作
{ type : 'ADD_TODO' }

// 因为为了完成添加任务的动作,需要一个任务名称,所以,动作中可以携带完成该动作需要的数据
{ type: 'ADD_TODO', name: '吃饭' }

// 删除任务的动作
{ type: 'DELETE_TODO', id: 3 }

reducer

reducer 使用来完成 action 的

reducer 实际上就是一个普通的JS函数

该函数能够接受两个参数: (state, action ) => newState

作用:根据 旧状态 和 action(动作) ,来计算出新的状态

注意:reducer 一定要有返回值!

注意:在reducer 中不要直接修改 state,应该创建新的 state (状态不可变原则

注意:reducer 应该是一个纯函数(同样的输入,必定得到同样的输出),不要有修改参数、调用 Math.random() 等不纯的操作。

// action => { type: 'ADD_TODO', name: '吃饭' }
// 假设 state => [{}, {}]
const reducer = (state, action) => {
  switch(action.type) {
    case 'ADD_TODO':
      // 1 执行 添加任务 的逻辑代码
      // state.push() 不要这么做!!!
      // 2 返回 添加任务 后的新状态
      return [...state, { name: action.name }]
    case 'DELETE_TODO':
      // 1 执行 删除任务 的逻辑代码
      // 2 返回 删除任务 后的新状态
      return state.filter(item => item.id !== action.id)
    default:
      // 如果当前 reducer 遇到无法处理的 action,就会执行 default
      // 此时,应该直接返回 state
      return state
  }
}

store

注意:一个 redux 应用中只有一个 store (仓库)

作用:将 action 和 reducer 组合在一起

职责:

  1. 提供整个应用的 state
  2. 提供 dispatch 方法,用来触发 action
  3. 提供 getState 方法,用来获取整个应用的 state
  4. 提供 subscribe 方法,监听 state 变化。
import { createStore } from 'redux'
// 将 reducer 作为参数传递给 store,那么,store 中就可以拿到 reducer
const store = createStore( reducer )

// 通过 store 中的 dispatch 分发任务
// dispatch 的参数:就是 action (动作)
store.dispatch( { type: 'ADD_TODO', name: '睡觉' } )

// 获取状态:
const state = store.getState()

// 这个返回值,用来取消本次订阅,取消后,状态变化时,就不会再执行该回调函数了
const unsubscribe = store.subscribe(() => {
  console.log('当前状态为:', store.getState())
})

// 取消订阅
unsubscribe()

redux 的执行过程

  • 在创建 store 的时候,redux会自动触发一次 reducer,目的:为了得到初始状态
// 模拟 redux 内部初始化操作:
reducer(undefined, { type: '@@redux/INITx.n.f.s.j.5' })

function reducer(state = 0, action) {
  // state 此时为默认值: 0 (因为第一次 redux 传入的值为 undefined)
  // action ==> { type: '@@redux/INITx.n.f.s.j.5' }

  switch (action.type) {
    case 'ADD':
      return state + 1
    default:
      // 因为该 reducer 无法处理 redux 生成的随机 动作类型
      // 所以,执行了此处的 default,也就是返回了 默认状态:0
      return state
  }
}

// 最终调用 store.getState() 得到了初始状态:0
  1. 触发动作:store.dispatch(action),dispatch 方法将 action 和 state 传递给 reducer。

  2. 实现动作:reducer 根据接收到的 action 和 state,得到最新的 state,并返回。

  3. 更新 state:store 接收到 reducer 返回的新 state,更新 store 中的 state。

Redux状态管理_第2张图片

react-redux

如果需要将 react 和 redux 配合到一起使用,就需要借助于这个 绑定库 react-redux

yarn add react-redux

核心API

  • Provider 组件
    • 1 导入 Provider 组件
    • 2 使用 Provider 组件包裹整个应用
    • 3 提供 store 属性,值为:redux中 createStore 方法创建的store
    • 这样的话,将来就可以在 组件 中,通过 props 来获取到 store 提供的state和操作状态的方法了
<Provider store={ store }>
  <App />
</Provider>
  • connect 函数

注意:仅仅使用 Provider 组件包裹整个应用,组件是无法直接获取到 redux 中的state 和操作state的方法的!!!
所以,如果要在组件中获取到 redux 中的state和操作state的方法,就使用 connect 函数来包裹组件,那么,组件中就可以获取到 redux 中的内容了

  • 其实,connect 函数就是一个 高阶组件!!!
// 第一次调用:可以传入一些配置
// 第二次调用:用来包装想要获取到 redux 状态的组件
connect()(Counter)

// 第一个函数:用来为组件提供状态
// 第二个函数:用来为组件提供操作状态的方法
connect(mapStateToProps, mapDispatchToProps)(Counter)

添加 mapStateToProps 和 mapDispatchToProps 后的使用

// 通过 count 来存储状态
const mapStateToProps = state => {
    return {
        count: state
    }
}

// 在原本使用 dispatch 的地方,用 increment 来替换
const mapDispatchToProps = dispatch => {
    return {
        increment: dispatch
    }
}

Counter = connect(
    mapStateToProps,
    mapDispatchToProps
)(Counter)

action 进阶

action creator

  • 推荐使用 action creator 来创建 action
  • action creator 是一个普通的函数,返回值为 action
  • 目的:简化 action 的书写
const increment = () => ({ type: 'INCREMENT' })
// 使用:
store.dispatch( increment() )

const addTodo = (name) => ({ type: 'ADD_TODO', name })
// 使用:
store.dispatch( addTodo('吃饭') )
// 相当于:
// store.dispatch( { type: 'ADD_TODO', name: '吃饭' } )
// store.dispatch( { type: 'ADD_TODO', name: '吃饭' } )

actionTypes

  • 将 action 的类型抽离到独立的 actionTypes.js 文件中
  • 目的:简化 action 类型的书写,避免使用时出错
  • 文件名称: constants.js / actionTypes.js
// actionTypes.js
const ADD_TODO = 'ADD_TODO'
const DELETE_TODO = 'DELETE_TODO'

export { ADD_TODO, DELETE_TODO }

// actions.js
import { ADD_TODO } from './actionTypes'
// 使用action creator:
const addTodo = (name) => ({ type: ADD_TODO, name })

// reducers.js
import { ADD_TODO } from './actionTypes'

const reducer = (state, action) => {
  switch(action.type) {
    case ADD_TODO:
      ...
  }
}

reducer 进阶

拆分 reducer

注意每个 reducer 只负责管理全局 state 中它负责的一部分。
每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性,并用于专门处理不同的数据域。

// reducers/todos.js
const todos = (state = [], action) => newState

// reducers/visibilityFilter.js
const visibilityFilter = (state = 'all', action) => newState

// reducers/loading.js
const loading = (state = false, action) => newState

合并 reducer

  • combineReducers 用来将多个 reducer 合并为一个 根reducer 作为 createStore 的参数
  • 注意:每个 reducer 中的状态最终被全部合并到一个对象中。对象中的键就是参数对象的键
// 此时的应用的状态为:{ todos: [], visibilityFilter: 'all', loading: false }
const rootReducer = combineReducers({
  todos,
  visibilityFilter,
  loading
})

const store = createStore(rootReducer)

// 该写法等价于上述combineReducers调用:
function rootReducer(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  }
}

combineReducer

  • 使用 combineReducer 函数来合并多个 子reducer,生成一个 根reducer
  • 只要通过 redux 分发了一个动作,redux 就会先调用 根reducer;根reducer 就会分别调用每一个 子reducer。由 每个子reducer来分别处理当前动作,能够处理的就返回新状态,不能处理的就返回默认状态

两种类型的组件

  • 展示组件:普通的 react 组件,与 redux 不直接关联
    • 展示组件,只负责 组件结构和样式( React 的内容 )
  • 容器组件:与 redux 关联的组件(使用 connect 高阶组件包装的组件)
    • 容器组件,负责与 redux 交互,负责获取 state,负责 dispatch 分发 action 来修改状态
    • 容器组件没有 JSX 结构

createStore() 方法

createStore(reducer, [preloadedState], enhancer) createStore说明

之前我们使用的时候只是使用了第一个参数 reducer ,我们来说一下第二个参数preloadedState 这个参数一般是用来设置 store 的默认值(初始时的 state)

就比如说:我们使用了 combineReducer 之后生成了一个 根reducer ,同样的会把这几个reducer的state合并为一个对象,这个对象就是 store 的state,我们这里可以使用第二个参数来个这个state设置初始值。

第三个属性,Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。我们在之后的中间件中会讲到。

// 获取localStorage 中存储的数据
const initialState = JSON.parse(localStorage.getItem("todos")) || {}

// 把初始数据作为第二个参数传入 createStore
const store = createStore(reducer, initialState)

// 监听状态修改时,自动把数据写入localStorage中
store.subscribe(() => {
    localStorage.setItem("todos", JSON.stringify(store.getState()))
})

Redux 与 路由搭配

  1. 安装路由 react-router-dom
  2. 使用 Provider 包裹 Router
<Provider store={store}>
    <Router>
    	<Switch>
                <Route exact path="/" render={() => <Redirect to="/all" />} />
                <Route exact path="/:filter?" component={App} />
         </Switch>
	</Router>
</Provider>

该路由规则 /:filter 能够匹配的路径为: /all 或 /active 或 /completed 、/123 不能匹配: / 或 /all/123

如何需要匹配 / ,可以在路由规则后面添加 ?,表示当前参数可选,

也就是: /:filter?,此时,该路由规则可以匹配:/ 或 /all 或 /active 或 /completed

redux 中的 connect

  • mapStateToProps 方法
// 第一个参数:表示 redux 中的状态
//  只要 redux 中的 state 变了,那么,这个函数就会被重新调用
// 第二个参数:表示 高阶组件connect() 接收到的属性
//  只要 高阶组件connect() 接收到新的属性,那么,这个函数也会别重新调用
const mapStateToProps = (state, ownProps) => ({})

什么时候我们会用到第二个参数呢?

比如:当我们需要获取一些参数,比如路由数据信息中的某些数据时,我们可以通过路由中的 withRouter 高阶组件来包裹一下 某个容器组件

const mapStateToProps = (state, ownProps) => {
    console.log(ownProps)
}
...

const TodoListContainer = connect(
    mapStateToProps
)(TodoList)

export default withRouter(TodoListContainer)

Redux 中间件

中间件,可以理解为处理一个功能的中间环节。
下图中,自来水从水库到用户家庭中的每一个环节都是一个中间件。
比如:Express 中间件,可以在接收到请求和响应这些请求之间,执行一些操作。
中间件的优势:可以串联,在一个功能中使用多个中间件。

Redux状态管理_第3张图片

Redux 中间件原理:创建一个函数,包装 store.dispatch,使用新创建的函数作为新的 dispatch。

注意:使用了 中间件 以后,我们通过代码调用 store.dispatch 就不是 redux 自身提供的 dispatch 了。而是由 中间件 提供的dispatch。每个 中间件 都在 dispatch 外面包了一层。最里面才是 redux 自身提供的 dispatch。

Redux状态管理_第4张图片

如果有多个中间件:

Redux状态管理_第5张图片

// 没有中间件:
// 注意:因为没有中间件所以,此处的 store.dispatch 就是 redux 自己提供的dispatch
store.dispatch( action ) --> reducer --> newState

// 有中间件
// 注意:有了中间件以后,store.dispatch 就不再是 redux 自己提供的 dispatch
store.dispatch( action ) --> 中间件1 -> 中间件2 --> reducer --> newState

Redux状态管理_第6张图片
Redux状态管理_第7张图片

常用中间件

  • 1 记录日志中间件:yarn add redux-logger
  • 2 常用插件:yarn add redux-devtools-extension
    • 使用方式,参考文档
import { composeWithDevTools } from 'redux-devtools-extension'

createStore(reducer, initialState, composeWithDevTools(applyMiddleWare(logger)) )
  • 3 异步数据流中间件: redux-thunk

我们可以使用 createStore的第三个参数 来使用中间件,比如 applyMiddleWare(logger) 这样我们就可以在控制台中看到我们的执行前后的数据状态

我们可以自己来写一个中间件,就以logger 为例

const logger = store => next => action => {
    // 记录日志代码
    console.log("1 dispatching", action)
    // 如果只使用了一个中间件:
    // 那么,next 就表示原始的 dispatch
    // 也就是:logger中间件包装了 store.dispatch
    let result = next(action)
    // 记录日志代码
    console.log("3 next state", store.getState())
    return result
}

Chrome浏览器中有一个 插件,可以检测redux的数据状态,但是使用这个插件的时候,需要我们使用 redux-devtools-extension 这个库,并且使用 composeWithDevTools 函数来调用一下 上面的 applyMiddleWare ,代码如上面所示。

注意:使用 redux-logger 中间件的时候需要把 logger 放在最后一个,不然会导致 thunkPromise执行的不是实际的结果(Note: logger must be the last middleware in chain, otherwise it will log thunk and promise, not actual actions )

异步数据的处理

redux-thunk 的使用步骤

  • 1 安装:yarn add redux-thunk
  • 2 导入:import thunk from 'redux-thunk'
  • 3 添加为 redux 的中间件
createStore(..., .., applyMiddleWare(thunk, logger))
  • 4 在 actions 中,创建异步action,来封装异步操作
const addTodoAsync = (text) => {
  return dispatch => {
    setTimeout(() => {
      // 分发同步action
      dispatch({ type: 'ADD_TODO', text })
    }, 2000)
  }
}

// 组件分发异步action:
// 注意:此处的 dispatch 是 thunk中间件 包装后的 dispatch
// 而 thunk 中间件能够处理 addTodoAsync 中返回的函数
props.dispatch( addTodoAsync('吃饭') )

同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。

  • 操作发起时的 Action
  • 操作成功时的 Action
  • 操作失败时的 Action

比如:获取服务器数据为例

{type: 'GET_DATA_START'}
{type:'GET_DATA_SUCCESS', result: res}
{type:'GET_DATA_ERROR', error: message}
  • 操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染
  • 操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染

下面来用一个简单的代码来看一下

在 actions 中添加一个 异步的 action creator

// 异步获取数据
const getDataAsync = () => {
    return async dispatch => {
        dispatch({ type: "GET_DATA_START" })
        try {
            const res = await axios.get("http://localhost:8081/todos")
            dispatch({ type: "GET_DATA_SUCCESS", todos: res.data })
        } catch (e) {
            dispatch({ type: "GET_DATA_ERROR", err: e })
        }
    }
}

在 reducer 中添加相应的处理逻辑,这里使用了 合并reducer 的方式,你也可以用一个reducer,但是state是一个对象

const todos = (state = [], action) => {
    switch (action.type) {
        case "GET_TODOS_START":
            return state
        case "GET_TODOS_SUCCESS":
            return action.todos
        case "GET_TODOS_ERROR":
            return state
        default:
            return state
    }
}

// 是否显示加载中
const loading = (state = false, action) => {
    switch (action.type) {
        case "GET_TODOS_START":
            return true
        case "GET_TODOS_SUCCESS":
        case "GET_TODOS_ERROR":
            return false
        default:
            return state
    }
}

const error = (state = "", action) => {
    switch (action.type) {
        case "GET_TODOS_ERROR":
            return "network error"

        default:
            return state
    }
}

const rootReducer = combineReducers({
    todos,
    loading,
    error
})

export default rootReducer

在容器组件中 把方法映射到组件 props 上

const mapStateToProps = state => {
    console.log(state)
    return {
        data: state.data,
        loading: state.loading,
        error: state.error
    }
}
const mapDispatchToProps = dispatch => {
    return {
        getDataAsync() {
            return dispatch(getDataAsync())
        }
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(List)

在展示组件中使用

class List extends Component {
    componentDidMount() {
        this.props.getDataAsync()
    }
    render() {
        const { data, loading, error } = this.props
        return (
            <>
                {loading ? <p>数据加载中...</p> : null}
                {error ? <p>{error}</p> : null}
                <ul>
                    {data.map(item => (
                        <li key={item.id}>
                            <span>{item.text}</span>
                        </li>
                    ))}
                </ul>
            </>
        )
    }
}

redux-devtools

  • redux 开发者工具

redux异步选型的其他方案

  • redux-thunk
  • redux-saga
  • redux-observable
  • Redux异步方案选型
  • 优雅地减少redux请求样板代码
  • 选型:redux-thunk + redux-promise-middleware
    • redux-promise-middleware
    • promise-middleware 与 thunk 代码对比
    • redux-thunk
  • 其他:redux-saga / redux-observable / redux-promise / redux-actions
  • 继承解决方案: dva

你可能感兴趣的:(前端开发)