组件很多时, 组件之间的数据传输和管理会很麻烦, 我们需要一个数据层框架. 目前最好的数据层框架是redux.
我们平常写程序时,对于复杂程序,有时会在很多函数之间将一些参数传来传去。如果我们直接将这些参数定义成所有函数都能直接访问到的全局变量,就省的到处传参了。redux 其实就是创建了这么一个所有组件都可以访问的到的全局变量。Redux 通过共享状态,来解决组件之间的复杂通信问题。它要求我们把所有的数据都放到公共的名为Store
的存储区域. 当Store
发生变化, 其他组件会感知到.
Redux = Reducer + Flux, 是两个概念的结合.
首先, Redux 中最基本的三个对象是: store, ruducer 和 action. Store 最重要的三个方法是 getState
, dispatch
和 subscribe
. 这些在下面都会提到, 需要特别注意.
简单来说:
getState
用于获取全局 state. dispatch
用于向 store 发送信息 (action), 用于对全局 state 进行修改. subscribe
用于向 store 注册一些特殊的函数, store 在处理完 action 后会接着调用这些函数, 主要用于全局 state 更新后, 使组件重新计算和渲染.接下来所讲的 redux 的整个工作流程, 就是围绕着上面的三个对象, 以及 store 对象的这三个方法.
Redux 中的核心是 store, 一个应用中应该只设置一个 store, 一般定义在单独的 store.js
文件中方便其他组件 import. store 中的东西可以被整个应用的所有组件访问到, 我们可以用它对组件们的 state 进行统一管理. 我们一般把组件们的 state 合并成一个全局 state, 作为 store 的 state. Store 需要通过 redux 库中的 createStore(reducer)
这一方法被创建. 其中, reducer 是用来处理 state 更新的逻辑, 在本文的下一部分中会细说. createStore
还有其实两个后续可选参数: preloadedState 和 enhancer, 分别为初始 state 和 enhancer, 这里就不过多展开了.
到这里, 我们只是定义了一个存储全局 state 的地方, 还没有说组件如何与 store 进行通信. 对于 store 中存储的全局 state, 我们需要考虑三种操作: 初始化, 更新和获取. 其中 state 的更新是关键.
State 的更新离不开 store 上的两个操作, dispatch
和 subscribe
. 先从 store.dispatch
讲起:
先假设我们 store 中的 state 已经初始化好了, 那么我们如何对它进行更新呢? Rudux 中, 修改 store 中的全局 state 的方式只有一种, 那就是调用 store.dispatch(action)
.
Action 是一个普通对象, 通过 store.dispatch
来传递给 store, 然后 store 会根据不同的 action 对存储的全局 state 进行不同的修改. redux-thunker 使得 action 可以为函数, 使用户可以把异步函数作为 action. redux-saga 类似 redux-thunker, 但是它单独将异步操作拆分出来放到专门的文件中进行管理.
根据不同 action 对全局 state 进行不同的修改, 这一逻辑是写在 reducer 中, 也需要用户自己去定义. Reducer 是个纯函数, 它接受之前的状态和和一个action, 并且返回新的状态. store 会把 reducer 处理后的返回值作为新的状态. Store 离不开 reducer, 离了 reducer store 就不知道要怎么根据不同 action 来修改全局 state, 所以 reducer 在 store 被创建时就要设置好: const store = createStore(reducer)
. store.dispatch
方法中重要的一步就是将前一 state 和 action 传递给给 reducer, 然后把 reducer 的返回值作为新的 state, 见下图中的第一个红框.
接着是 store.subscribe
:
前面我们讲到, 在修改全局 state 的操作中, 组件通过 store.dispatch(action)
向 store 发出修改全局变量的 action, dispatch
方法会调用 reducer 函数并将其返回值作为新的全局 state. 但是, 这仅仅是 store 中的一个对象发生了变化, 网页上渲染的组件并没有发生任何变化. 我们需要让 store 中全局 state 的变化传播到相关组件, 并让组件根据全局 state 的变化来进行重新渲染.
Redux 中通过观察者模式来实现这一操作. 我们可以通过 store.subscribe(callback)
来向 store 注册一些回调函数, 这些回调函数会在 store 处理完 action 后被调用. 所以我们可以利用这一方法向 store 注册这种函数: 利用 store 中的全局 state 来重新计算和渲染组件的函数.
React 组件有两种, 一种是通过 class 定义的组件, 一种是通过 function 定义的组件. 对于类组件, 当组件调用 this.setState(newState)
时, 如果 newState 与之前的 state 不一样, 就会引发组件的重新计算和渲染. 对于函数组件 + hooks, 调用setXXX
会引起组件的重新计算和渲染. 我们可以将他们放在要注册的回调函数中, 并将从 store 中获取到的新的全局 state 作为参数传给 setState
. 以类组件为例, 最终要注册的回调函数大概是这样的: () => this.setState(store.getState())
. 这样一来, store 处理完 action 对全局 state 修改后, 紧接着调用这一函数, 组件就会利用新的全局 state 来设置自己的 state, 从而触发组件的重新计算和渲染.
此外, 要注意, 在使用 React 的 Strict Mode 的时候, 不能直接在 constructor
中 subscribe, 否则每次 dispatch 的时候, subscribe 的函数会调用两遍, 且 store 初始化后的首次 dispatch 时调用的第一遍会报错, 错误大意是不能在未挂载的组件上调用setState
. 这是因为…
react-redux 这一框架可让帮我们 subscribe.
初始化的方式有两种, 第一种是在 store 创建时作为第二个参数传入 ( createStore(reducer, preloadedState)
), 第二种是利用 es6 默认参数来设置 reducer 的参数(function reducer(state=defaultState, action) {}
). 若未用以上两种方式初始化就会得到 undefined
.
但是事实上当你对刚创建好的 store 调用 getState()
, 却很有可能会得到与 preloadedState 不一样的结果, 这是因为 store 在初始化过程中, 在设置当前 state 为 preloadedState (默认为 undefined
) 之后, 会调用一次 dispatch
, 而 dispatch 的 action 是 redux 内置的一个专门用来表示初始化的 action: ActionTypes.INIT
, 一个随机字符串. 所以我们得到的 state 实际上是被 reducer 处理过一遍的 reducer 了.
所以, 我们在写 reducer 的逻辑时, 要特别注意这一点. 如上图所示, ActionType.INIT
的值是随机的, 我们不能直接针对它来进行特殊处理. 所以我们一般在 reducer 的最后, 在 action 类型未知不被任何条件语句捕获的情况下, 直接返回当前 state. reducer.js
如下:
const defaultState = {state1: 1}
export default (state = defaultState, action) => {
if(action.type = ACTION1) {
// compute new state ...
return newState;
} else if (action.type = ACTION2) {
// compute new state ...
return newState;
}
return state;
}
获取 store 中的全局 state 这一操作, 是通过 store.getState()
来实现的. 这一方法会返回 store 当前的状态, 它等同于 reducer 最后一次被调用时的返回值. 像上部分所说的那样, store 在初始化过程中就会调用一次 reducer, 所以不存在 reducer 没有在 getState
前调用的情况.
创建 Reducer, src/store/reducer.js
. Reducer 用来处理 action 并设置新的ation的状态, 并返回新的 state. 注意最后直接把之前的 state 作为默认返回值. 注意逻辑中不要对 state 里的属性做修改后直接返回: 这样返回的指针还是之前的指针, 在后续进行 store 中 state 得更新时, 不会引起组件的重新渲染. React 中修改 state 一般是先深复制一份, 再对复制后的 state 进行修改并返回.
const defaultState = {
initialValue: 'initial value',
list: ["c", "java", "javascript"],
}
export default (state = defaultState, action) => {
if(action.type === 'change_input_value') {
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
console.log(newState);
return newState;
} if(action.type = "2") {
// ...
return newState;
}
return state;
}
利用 Reducer 创建 Store, src/store/index.js
.
import { createStore } from "redux"
import reducer from './reducer'
const store = createStore(reducer);
export default store;
组件使用 Store 的初始 state, 初始化自己的 state, src/component/Todolist.js
.
class TodoList extends Component {
constructor(props){
super(props);
this.state = store.getState()
}
}
组件监听 Store 中全局状态的更新, src/component/Todolist.js
. 注意, Strict Mode 下, 在 constructor 进行 subscribe 会出错.
import {Component} from 'react'
import store from '../store'
class TodoList extends Component {
componentDidMount(){
store.subscribe(() => this.setState(store.getState()));
}
}
组件向 Store 发出更新请求, src/component/Todolist.js
. Store 中的 reducer 根据 action 对全局 state 进行更新, 然后触发上一步 subscribe 的函数, 用新的全局状态来更新组件自己的状态, 从而引起组件的重新计算和渲染.
handleInputChange = (e) => {
const action = {
type: 'change_input_value',
value: e.target.value,
}
store.dispatch(action);
}
当 ation 的类型有很多时, component 代码中的 aciton 的 type 的单词容易写错, 写得和 reducer 内的判断逻辑不一致, 这种字符串错误也不会在控制台报错, 很难 debug.
为了解决上述问题, 引入了 actionTypes.js
文件:
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM = 'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
之后再在 component 和 reducer 中引用即可.
这样一来, 如果单词拼错, 会直接报编译错误.
把 action 放在 actionCreators 里面统一管理. 提高了代码的可维护性, 方便自动化测试. component 文件中无需再导入 actionTypes.
创建 action creators, src/store/actionCreators.js
:
import { CHANGE_INPUT_VALUE } from "./actionTypes";
export const getInputChangeAction = (value) => ({
type: CHANGE_INPUT_VALUE,
value: value,
})
使用 action creators, src/component/TodoList.js
import { getInputChangeAction } from "../store/actionCreators.js"
handleInputChange = (e) => {
const action = getInputChangeAction(e.target.value);
store.dispatch(action);
}
随着你的应用程序变得越来越复杂,你会希望将你的 reducing 函数拆分到每个组件中去,每个函数管理 state 的一个独立的部分。combineReducers
辅助函数可以将多个不同的 reducing 函数, 合并为可以传递给 createStore
的单个 reducing 函数。生成的 reducer 调用每个子 reducer,并将其结果收集到单个 state 对象中。combineReducers()
会创建一个全局 state, 每个子 reducer 创建的 state 是全局 state 的一部分, 位于自己的命名空间下.
在每个组件目录下, 创建一个 store 目录, 用来保存自己相关的 reducer逻辑以及相应的 actionTypes, actionCreators. 写法与之前完全相同.
actionTypes 也可以写成componentName/ACTION_NAME
的形式, 方便在使用Redux DevTools 时区分来自不同组件的 action.
export const Search_Focus_Action = 'header/SEARCH_FOCUS';
export const Search_Blur_Action = 'header/SEARCH_BLUR';
index.js
.
import reducer from "./reducer";
export { reducer };
在全局的store的reducer里, 使用 combineReducers
import { combineReducers } from 'redux';
import {reducer as headerReducer} from '../components/Header/store';
export default combineReducers({
header: headerReducer
})
store 是唯一的.
整个应用中只有一个 store 公共存储空间.
只有 store 能够改变自己的内容.
reducer 返回给 store 一个新的 state, store 拿到 reduer 的数据后, 自己对自己的内容进行更新. 所以不要在 reducer 中直接对传入的 state 进行操作, 修改前先用 JSON.PARSE(json.stringify(state)
复制一份.
reducer 必须是纯函数(pure function).
纯函数指的是, 给定固定的输入, 就一定会有固定的输出, 而且不会有任何副作用.
一旦一个函数中有 setTimeout
, Ajax 请求或是和时间相关的操作, 它就不是纯函数.
具体到 reducer, 是指当传入的 state 和 action 固定时, 返回的结果是固定的. 一个不固定的例子如下:
import CHANGE_INPUT_VALUE from "./actionTypes"
export default (state = defaultState, action) = {
if (action.type === CHANGE_INPUT_VALUE) {
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = new Date(); // 不固定
return newState;
}
// ...
}
reducer 中一旦对参数直接进行了修改, 就产生了副作用. 一个具有副作用的例子如下:
import CHANGE_INPUT_VALUE from "./actionTypes"
export default (state = defaultState, action) = {
if (action.type === CHANGE_INPUT_VALUE) {
state.inputValue = '';
const newState = JSON.parse(JSON.stringify(state));
return newState;
}
// ...
}
待更
修改代码中的store
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
参考资料