提醒: 本节中,所有赋值语句均可以使用解构赋值

action

Action 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

在dispatch一个action时候,action中包含action类型和载荷:

  1. dispatch({
  2. type: 'login'
  3. payload: {},
  4. });

action的本质是普通的js对象。且有以下约定:

  • action中必须有一个 type 字段来表示action的类型
  • action中的其他字段参照Flux 标准 Action

在这个todo app中,可以定义下面的action

  1. {
  2. type: ''
  3. }

action创建函数

action创建函数本质是一个返回action对象的js函数。

在redux中,创建函数只返回一个简单的action:

  1. export function addTodo(todoObj) {
  2. return {
  3. type: 'addTodo',
  4. text: todoObj.text,
  5. status: '-1',
  6. index: todoObj.index,
  7. }
  8. }

这样做使创建函数更容易移植和测试。
传统给flus实现中,调用创建函数时一般还会发送一个 dispatch :

  1. function addTodoWithDispatch(text) {
  2. const action = {
  3. type: ADD_TODO,
  4. ...text
  5. }
  6. dispatch(action)
  7. }

redux中发送dispatch只需要将创建函数的结果传给 dispath()

  1. dispatch(addTodo(text))

或者创建一个 被绑定的 action 创建函数 来自动 dispatch:

  1. const boundAddTodo = text => dispatch(addTodo(text))
  2. const boundCompleteTodo = index => dispatch(completeTodo(index))

然后直接调用

  1. boundAddTodo(text);
  2. boundCompleteTodo(index);

store中可以直接通过 store.dispatch 来发起dispatch,但通常做法是使用redux-react中的 connect() 帮助器来调用。bindActionCreators() 可以自动把多个 action 创建函数 绑定到 dispatch() 方法上。

Action 创建函数也可以是异步非纯函数。

reducers

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

设计state结构

在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来?
以 todo 应用为例,需要保存两种不同的数据:

  • 当前选中的任务过滤条件;
  • 完整的任务列表。

通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。

  1. const state = {
  2. visible: "showAll",
  3. todos: [
  4. {
  5. text: "上班打卡",
  6. status: 1,
  7. },
  8. {
  9. text: "下班打卡",
  10. status: -1,
  11. }
  12. ],
  13. }

处理 Reducer 关系时的注意事项

开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 todosById: { id -> todo }todos: array<id> 是比较好的方式,本文中为了保持示例简单没有这样处理。

对上面注意事项的附加内容 实际开发中,数据关联交给后端,前端只负责获取数据以及展示数据。后端在查库的时候通过id关联来返回数据,前端获取到的数据,如果有id需求,那么得到的数据就是有id字段。比如从后端拿去系统里所有用户信息,每个用户都需要有唯一id。如果获取的数据不需要有id,比如一些状态字段,这时候得到的数据就不会有id字段。
通常情况下,每个表都会有一个id字段。

action处理

确定action结构后,就可以开发reducer。reducer是一个纯函数,接收action和旧的state,返回新的state。reducer中 禁止 进行以下动作:

  • 修改传入参数
  • 执行有副作用的函数
  • 调用非纯函数 ```javascript const initState = { visible: “showAll”, todos: [ {
    1. text: "上班打卡",
    2. status: 1,
    }, {
    1. text: "下班打卡",
    2. status: -1,
    } ], }

function todoApp(state, action) { if(typeof action === “undefined”) { return initState; } switch(action.type) { case “addTodo”: { return { …state, todos: [ …state.todos, { text: action.text, status: action.status, } ] } } case “setVisible”: { return { …state, visible: action.visible, } } case “toggleStatus”: { const newTodos = state.todos.map(current => { if(action.index == current.index) { return { …current, status: current.index + 1, } } return current; }) return { …state, …newTodos, } } default: { return initState; } } }

export default todoApp;

  1. 注意到上面的reducer中, `setVisible` 只设置过滤状态,并不参与待办的增加和状态转换,因此可以将其拆分:
  2. ```javascript
  3. function todos(state = [], action) {
  4. switch (action.type) {
  5. case ADD_TODO:
  6. return [
  7. ...state,
  8. {
  9. text: action.text,
  10. completed: false
  11. }
  12. ]
  13. case TOGGLE_TODO:
  14. return state.map((todo, index) => {
  15. if (index === action.index) {
  16. return Object.assign({}, todo, {
  17. completed: !todo.completed
  18. })
  19. }
  20. return todo
  21. })
  22. default:
  23. return state
  24. }
  25. }
  26. function visibilityFilter(state = SHOW_ALL, action) {
  27. switch (action.type) {
  28. case SET_VISIBILITY_FILTER:
  29. return action.filter
  30. default:
  31. return state
  32. }
  33. }
  34. function todoApp(state = {}, action) {
  35. return {
  36. visibilityFilter: visibilityFilter(state.visibilityFilter, action),
  37. todos: todos(state.todos, action)
  38. }
  39. }

redux提供了 combineReducers 来做上面 todoApp 做的事情:

  1. import { combineReducers } from 'redux'
  2. const todoApp = combineReducers({
  3. visibilityFilter,
  4. todos
  5. })
  6. export default todoApp

注意上面的写法和下面完全等价:

  1. import { combineReducers } from 'redux'
  2. const todoApp = combineReducers({
  3. visibilityFilter,
  4. todos
  5. })
  6. export default todoApp

你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价:

  1. const reducer = combineReducers({
  2. a: doSomethingWithA,
  3. b: processB,
  4. c: c
  5. })
  6. function reducer(state = {}, action) {
  7. return {
  8. a: doSomethingWithA(state.a, action),
  9. b: processB(state.b, action),
  10. c: c(state.c, action)
  11. }
  12. }

combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。

在es6中, combineReducers 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export 暴露出每个 reducer 函数,然后使用 import * as reducers 得到一个以它们名字作为 key 的 object:

  1. import { combineReducers } from 'redux'
  2. import * as reducers from './reducers'
  3. const todoApp = combineReducers(reducers)

提示 reducers中的switch条件不一定必须是 action.type ,也可以是约定好的其余字段。但是action中type字段是必须有,否则应用会运行异常。

store

store负责将action和reducer联系到一起,有以下作用:

每个redux应用有且只能有一个store 。如果需要拆分逻辑,需要组合reducers而不是再开一个store。

  1. // 创建store
  2. import { createStore } from 'redux'
  3. import todoApp from './reducers'
  4. let store = createStore(todoApp)

createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。

  1. let store = createStore(todoApp, window.STATE_FROM_SERVER)

发起actions

  1. import * as actions from './actions/action';
  2. import store from './store';
  3. // 打印初始状态
  4. console.log(store.getState())
  5. // 每次 state 更新时,打印日志
  6. // 注意 subscribe() 返回一个函数用来注销监听器
  7. const unsubscribe = store.subscribe(() => console.log(store.getState()))
  8. // 发起一系列 action
  9. store.dispatch(actions.addTodo({
  10. text: '周末去哪玩',
  11. status: -1,
  12. }))
  13. // 停止监听 state 更新
  14. unsubscribe()

官网示例:

  1. import {
  2. addTodo,
  3. toggleTodo,
  4. setVisibilityFilter,
  5. VisibilityFilters
  6. } from './actions'
  7. // 打印初始状态
  8. console.log(store.getState())
  9. // 每次 state 更新时,打印日志
  10. // 注意 subscribe() 返回一个函数用来注销监听器
  11. const unsubscribe = store.subscribe(() => console.log(store.getState()))
  12. // 发起一系列 action
  13. store.dispatch(addTodo('Learn about actions'))
  14. store.dispatch(addTodo('Learn about reducers'))
  15. store.dispatch(addTodo('Learn about store'))
  16. store.dispatch(toggleTodo(0))
  17. store.dispatch(toggleTodo(1))
  18. store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
  19. // 停止监听 state 更新
  20. unsubscribe()

搭配react

在搭配react时,要考虑以下要点:

  • 事件处理:切换代办状态时要触发action,让reducer更改state
  • 获取state:state需要手动放到组件中,如果没有任何操作,state不会自己跑到组件state或者props中
  • 传入store:redux从stroe中获取state和actions,和state一样,store也不会自己跑到组件中

    获取state

    获取state通过容器组件来实现。容器组件将组件和redux关联起来。

技术上讲,容器组件就是通过 store.subscribe() 从state树中读取数据,并将其通过props提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。(这样你就不必为了性能而手动实现 React 性能优化建议 中的 shouldComponentUpdate 方法。)

  1. const mapStateToProps = (state) => {
  2. return {
  3. todos: state.todos,
  4. visible: state.visible,
  5. }
  6. }
  7. const ConnectedTodoApp = connect(mapStateToProps)(TodoApp);

除了读取state,容器组件还可以分发action。和state类似,可以定义 mapDispatchToProps 方法接收 dispatch 中的方法并返回想要注入要渲染的组件的props中的回调方法。

  1. const mapDispatchToProps = dispatch => {
  2. return {
  3. onTodoClick: id => {
  4. dispatch(toggleTodo(id))
  5. }
  6. }
  7. }

传入store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件 <Provider>魔法般的 让所有容器组件都可以访问 store,而不必显式地传递它。只需要在渲染根组件时使用即可。

  1. export default function() {
  2. return(
  3. <Provider store={store}>
  4. <ConnectedTodoApp/>
  5. </Provider>
  6. );
  7. }

效果

2020-09-14 21-53-15.mkv
2020-09-14 21-53-15.mkv_20200914_221055.gif

源码

  1. import { Button, Input, message, Table } from 'antd';
  2. import React, { useState } from 'react';
  3. import { connect, Provider } from 'react-redux';
  4. import store from './store';
  5. import * as actions from './actions/action';
  6. /* eslint-disable eqeqeq */
  7. function TodoApp(props) {
  8. const [value, setValue] = useState("");
  9. function onChange(e) {
  10. if(e) {
  11. e.preventDefault();
  12. }
  13. setValue(e.target.value);
  14. }
  15. function renderOptions(text, index) {
  16. const { dispatch } = props;
  17. if(text.status === "-1") {
  18. return(
  19. <div>
  20. <Button onClick={() => dispatch(actions.toggleStatus({
  21. index: index + 1,
  22. status: "0",
  23. }))}>开始</Button>
  24. <Button onClick={() => dispatch(actions.toggleStatus({
  25. index: index + 1,
  26. status: "1",
  27. }))}>完成</Button>
  28. <Button onClick={() => dispatch(actions.toggleStatus({
  29. index: index + 1,
  30. status: "-1",
  31. }))}>重置</Button>
  32. </div>
  33. );
  34. }
  35. if(text.status == 0) {
  36. return(
  37. <div>
  38. <Button onClick={() => dispatch(actions.toggleStatus({
  39. index: index + 1,
  40. status: "1",
  41. }))}>完成</Button>
  42. <Button onClick={() => dispatch(actions.toggleStatus({
  43. index: index + 1,
  44. status: "-1",
  45. }))}>重置</Button>
  46. </div>
  47. );
  48. }
  49. if(text.status == 1) {
  50. return(
  51. <Button onClick={() => dispatch(actions.toggleStatus({
  52. index: index + 1,
  53. status: "-1",
  54. }))}>重置</Button>
  55. );
  56. }
  57. }
  58. const addTodo = () => {
  59. const { dispatch } = props;
  60. if(value.length === 0 || !value) {
  61. message.warn("不允许添加空代办");
  62. return;
  63. }
  64. dispatch(actions.addTodo({
  65. text: value,
  66. index: props.todos.length + 1,
  67. status: "-1",
  68. }))
  69. }
  70. const columns = [
  71. {
  72. title: "序号",
  73. dataIndex: "index",
  74. width: "10%",
  75. },
  76. {
  77. title: "代办",
  78. dataIndex: "text",
  79. width: "60%",
  80. },
  81. {
  82. title: "状态",
  83. dataIndex: "status",
  84. width: "10%",
  85. filters: [
  86. {
  87. text: "未开始",
  88. value: "-1",
  89. },
  90. {
  91. text: "进行中",
  92. value: "0",
  93. },
  94. {
  95. text: "已完成",
  96. value: "1",
  97. },
  98. ],
  99. onFilter: (value, record) => value === record.status,
  100. render: (record, text) => {
  101. if(text.status === "-1") {
  102. return "未开始";
  103. }
  104. if(text.status === "1") {
  105. return "已完成";
  106. }
  107. return "进行中";
  108. }
  109. },
  110. {
  111. title: "操作",
  112. render: (text, record, index) => renderOptions(text, index)
  113. },
  114. ];
  115. return(
  116. <div>
  117. <Button onClick={addTodo}>新增代办</Button>
  118. <Input style={{width: 600}} onChange={onChange} value={value}/>
  119. <Table
  120. columns={columns}
  121. dataSource={props.todos}
  122. />
  123. </div>
  124. );
  125. }
  126. const mapStateToProps = (state) => {
  127. return {
  128. todos: state.todos,
  129. visible: state.visible,
  130. }
  131. }
  132. const ConnectedTodoApp = connect(mapStateToProps)(TodoApp);
  133. export default function() {
  134. return(
  135. <Provider store={store}>
  136. <ConnectedTodoApp/>
  137. </Provider>
  138. );
  139. }

注意事项

connect后的组件必须包裹在Provider中

如果 connect 返回的组件的父组件中没有包含 store 的 Provider 则会报错找不到 stroe.