本篇文章首先会介绍和分析Redux,然后从头开始,一步一步实现一个简单的Redux。
你应该知道的
什么是Redux
参考Redux中文官网:JS应用的状态容器,提供可预测的状态管理。它可以帮助你开发出行为稳定可预测的、运行于不同的环境(客户端、服务器、原生应用)、易于测试的应用程序。不仅于此,它还提供超爽的开发体验。
Redux经常和React搭配使用,但也可以单独使用。另外还有Redux团队开发的其他工具库:React-Redux和Redux Toolkit。
Redux中的重要概念
首先看这两张图,讲解一下里面的各个元素。
- Action Creator:View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。它是一种辅助函数,根据传入的参数中生成一个Action,为这个Action分配一个类型(type),并把这个Action传送给Dispatcher。
- Action:Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。
- Dispatcher:接受一个 Action 对象作为参数,将它发送出去,然后将Action的结果发送到Store。
- Store: 用于存储状态,Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
- State:如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
- View:用户视图,用于产生Action和接收渲染Store中状态的改变
- Reducer:Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
需要注意的几个点:
- 在Redux中,状态存储在store中。存储的状态通过 action 改变。 action 一个是对象,它至少有一个字段(type)确定操作的类型。
- Action 对应用程序状态的影响是通过使用一个 reducer 来定义的。 实际上, reducer 是一个纯函数,它以当前状态(state)和 action 为参数。 它返回一个基于传入的action的新状态(state)。state的状态不应该被修改,所以返回的state值应该是一个新创建的state。
- Reducer 不直接从应用程序中调用。 Reducer 只作为创建store,即 createStore 的一个参数给出。
- store 现在使用 reducer 来处理actions,这些action通过
dispatch
方法 被分派或“发送”到 store 中。 - store发生改变之后,由React来将改变渲染到页面上(View)。
因此,action 对应用程序状态的影响是通过使用一个 reducer 来定义的。 实际上,reducer 是一个函数,它以当前(previous) state 和 action 为参数,返回一个新的 state。
这里的createStore
和combineReducers
就是之后要实现的部分功能。多个reducer可以被结合到一起(combineReducers
),在结合的时候为每个reducer创建一个标识符,在action中可以使用该标识符获取对应reducer的state(即数据)。
为什么要有Redux
Redux是为了解决React组件间通信和组件间状态共享而提出的一种解决方案。
解决了什么问题
随着项目的越来越大,组件和组件的状态越来越多,嵌套层数越来越深,所以组件之间通信越来越复杂,项目将越来越难以维护。使用Redux之后,组件的状态都保存到store之中,各个组件可以直接从store之中获取到自己需要的状态。如果需要改变store中的状态,Redux也提供了dispatch方法,组件可以dispatch一个action,根据action的type属性,reducer会对状态做出变化,得到新的状态。因为全局只能有一个store,这样将全局状态保存到一处的做法,使得项目更加容易维护,组件之间的通信也更加容易实现和清晰。
-
组件间通信 由于connect后,各connect组件是共享store的,所以各组件可以通过store来进行数据通信,当然这里必须遵守redux的一些规范,比如遵守 view -> aciton -> reducer的改变state的路径。
-
通过对象驱动组件进入生命周期 对于一个React组件来说,只能对自己的state改变驱动自己的生命周期,或者通过外部传入的props进行驱动。通过Redux,可以通过store中改变的state,来驱动组件进行update。
-
方便进行数据管理和切片 redux通过对store的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现redo之类的操作。
使用场景
- 同一个 state 需要在多个 Component 中共享;
- 需要操作一些全局性的常驻 Component,比如 Notifications,Tooltips 等;
- 太多 props 需要在组件树中传递,其中大部分只是为了透传给子组件;
- 业务太复杂导致 Component 文件太大,可以考虑将业务逻辑拆出来放到 Reducer 中;
文档解释
可以参考官网文档When should I use Redux?。
Redux 的特点
- 单向数据流。View 发出 Action (
store.dispatch(action)
),Store 调用 Reducer 计算出新的 state ,若 state 产生变化,则调用监听函数重新渲染 View (store.subscribe(render)
)。 - 单一数据源,只有一个 Store。
- state 是只读的,不应该被改变,每次状态更新之后只能返回一个新的 state。
- 没有 Dispatcher ,而是在 Store 中集成了 dispatch 方法,
store.dispatch()
是 View 发出 Action 的唯一途径。 - 支持使用中间件(Middleware)管理异步数据流。
实现一个简单的Redux
基于上面的理解,从最基础的开始,一步一步加入新的功能,最终实现一个简易版的Redux。
我们还是用简单但经典的计数器作为例子。
项目可以用cra(creat-react-app)或其他脚手架生成。结构如下图所示:
其中,App代码如下:
import Counter from "./Counter";
import "./App.css";
function App() {
return (
<div className="app">
<h1>Redux implementation</h1>
<Counter />
</div>
);
}
export default App;
其他功能我们一步一步来实现。
0. App结构
基于依赖关系,App的整体结构从左到右,从最内层到最外层如下所示:
a. counterStore.js
首先,我们要创建一个store来定义对应的actions,action creators和reducers。两个action creators分别为incrementAction
和decrementAction
,它们有对应action和各自的type:INC
和DEC
。这里我们只需要一个Reducer就够了,处理两种的actions。
// 定义action creators和actions(types)
const incrementAction = () => ({ type: "INC" });
const decrementAction = () => ({ type: "DEC" });
// 定义reducer
const reducer = (state = 0, action) => {
if (action.type === "INC") {
return state + 1;
}
if (action.type === "DEC") {
return state - 1;
}
return state;
};
export default reducer;
export { incrementAction, decrementAction };
b. Coutner.js
然后在Coutner.js
组件内导入Actions,实现如下:
import React from "react";
import { connect } from "./redux-lib";
import { incrementAction, decrementAction } from "./counterStore";
import "./Counter.css";
const Counter = ({ value, increase, decrease }) => (
<div>
<p>Value: {value}</p>
<button onClick={increase}>Increment</button>
<button onClick={decrease}>Decrement</button>
</div>
);
// 将state转化为props(value)
const mapStateToProps = (state) => {
return {
value: state.counter
};
};
// 将dispatch(action)转化为props(increase和decrease)
const mapDispatchToProps = (dispatch) => ({
increase: () => dispatch(incrementAction()),
decrease: () => dispatch(decrementAction())
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
这里需要用connect
方法“连接”mapStateToProps
和mapDispatchToProps
这两个方法,简单用另外一张图来描述:
可以理解为mapStateToProps
返回的是新的state,而mapDispatchToProps
是基于旧的state和action来生成新state的过程,所以可以理解为“destroy”原来的state的一个过程。
现在这个组件可以通过它的props调用函数increase/decrease, 也就是基于action creator定义的action。这样一来我们就不需要单独调用 dispatch 函数,因为 connect 已经将 increase/decrease 的 action creator 修改为包含 dispatch 的形式了。
connect
函数将在后面实现。
c. App.js
然后再将Coutner
组件导入到App
中。
import Counter from "./Counter";
import "./App.css";
function App() {
return (
<div className="app">
<h1>Redux implementation</h1>
<Counter />
</div>
);
}
export default App;
d. index.js
最后再将App
导入到index.js
中。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider, createStore, combineReducers } from "./redux-lib";
import counterReducer from "./counterStore";
const rootReducer = combineReducers({
counter: counterReducer
});
const store = createStore(rootReducer);
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
这里导入了在步骤a中创建的reducer
,也就是counterReducer
,作为combineReducers
函数的参数。而combineReducers
生成的reducer集合又作为createStore
函数的参数,最终生成我们的store。最终,store作为Provider
的props,包在了App的外面。
这里的{ Provider, createStore, combineReducers }
将在后续部分一一实现。
1. createStore函数
参考Redux官网API文档:createStore
参数
reducer
(Function) : 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。- [
preloadedState
] (any) : 初始时的 state。你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用combineReducers
创建reducer
,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何reducer
可理解的内容。 enhancer
(Function) : Store enhancer。你可以选择指定它以使用第三方功能,如middleware、时间旅行、持久化来增强 store。Redux 中唯一内置的 store enhander 是applyMiddleware()
。
返回值
(Store
): 保存了应用程序所有 state 的对象。改变 state 的唯一方法是 dispatch action。你也可以 subscribe state 的变化,然后更新 UI。
实现
我们已经知道,createStore
函数的作用是接收reducer为参数,最后返回一个store对象。而这个store对象需要有三个基本方法(参考官方文档Store 方法):
- getState:返回应用当前的 state 树。 它与 store 的最后一个 reducer 返回值相同。
- dispatch(action):分发action,这是触发 state 变化的唯一途径。它返回一个可以解绑变化监听器的函数。
- subscribe(listener): 添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。
其中,listener
(Function)是一个每当 dispatch action 的时候都会执行的回调。state 树中的一部分可能已经变化。
这个返回的store会作为props提供给Provider
,而Provider
是由React.createContext()
生成的Context的Provider(生产者)组件(Context.Provider
),它作为一个父组件,包裹了各种子组件(Context.Consumer
),也就是我们的App。
不难发现,Context的使用基于生产者消费者模式。所以就有了上面store对象里的三种方法。Provider
会在后续详细讲解实现。
那么createStore
的实现如下:
const createStore = (reducer, initialState = {}) => {
let state;
let listeners = []; // listeners 是一组函数的集合(数组)
//getState 返回应用当前的 state 树。 它与 store 的最后一个 reducer 返回值相同。
const getState = () => state;
//subscribe 是一个向store中添加监听器的函数,返回一个可以解绑变化监听器的函数。
const subscribe = (listener) => {
// 添加监听器
listeners.push(listener);
// 返回一个可以解绑变化监听器的函数
return () => {
let index = listeners.indexOf(listener);
if (index >= 0) {
// 解绑变化监听器
listeners.splice(0, index);
}
}
};
// dispatch 函数使用reducer来获取新的state并通知每个监听器state发生了变化
// 将使用当前 getState() 的结果(上面d的state)和传入的 action 以同步方式的调用 store 的 reducer 函数。这个 reducer 的返回值会被作为下一个 state。
// 从现在开始,这个 state 就成为了 getState() 的返回值,同时每个变化监听器(change listener)会被触发。
const dispatch = (action) => {
state = reducer(state, action); // 新的state
listeners.forEach((listener) => listener(state)); // 并通知每个监听器state发生了变化
};
// 分发初始state
dispatch(initialState);
return { getState, dispatch, subscribe };
};
export default createStore;
具体逻辑详见注释。
2. combineReducers函数
参考Redux官网API文档:combineReducers
参数
reducers
(Object): 一个对象,它的值(value)对应不同的 reducer 函数,这些 reducer 函数后面会被合并成一个。
返回值
(Function):一个调用 reducers
对象里所有 reducer 的 reducer,并且构造一个与 reducers
对象结构相同的 state 对象。
那么它的实现如下:
// combineReducers函数将一个含有不同reducers的对象转化为一个单独的reducer,这个合成的reducer将作为参数传递给createStore函数
const combineReducers = (reducers) => {
// 函数返回值也是一个reducer,所以它也是一个返回函数的函数
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
}, {}); // reduce方法,从一个空对象开始reduce
};
};
export default combineReducers;
3. Connect函数
由前文的例子可知,connect()
函数是一个将一个React组件和一个Redux的store连接起来的函数。
参考React-Redux官网API文档:connect()
参数
实际上它可以接收四个参数,但这里我们只采用前两个:
-
mapStateToProps?
: (state, ownProps?) => Object参考React-Redux官网API文档:mapStateToProps
connect
函数接受所谓的mapStateToProps
函数作为它的第一个参数。这个函数可以用来定义基于 Redux 存储状态的连接组件的 props。如果
mapStateToProps
函数被定义为只接收一个参数(state),那么每次store的state发生变化时,它都会被调用,而state作为参数传递进去。如果
mapStateToProps
函数被定义为接收两个参数(state, ownProps?),那么每次store的state发生变化或包装组件接收到新props(对象浅比较)时,它都会被调用,而state作为参数传递进去。本篇没有实现这个。mapStateToProps
返回一个对象,通常会被定义为stateProps
,会合并为连接组件的props。如果connect函数使用了mergeProps函数,那么stateProps
会作为它的第一个参数。 -
mapDispatchToProps?
: Function((dispatch, ownProps?) => Object) | Objectconnect
函数的第二个参数可以为mapDispatchToProps
(可以是一个函数,或者一个对象,或者没有),它是一组作为props传递给连接组件的 action creator 函数。mapDispatchToProps
默认接收dispatch(store.dispatch)
为第一个参数。它应该返回一个对象,这个对象的每个属性应该是一个函数,它会分发(dispatch)一个action到store(也就是action creator函数)。这个对象被定义为dispatchProps
,它会被合并为连接组件的props。如果connect函数使用了mergeProps
函数,那么stateProps
会作为它的第二个参数。
返回值
connect()
函数的返回值是一个包装函数,这个包装函数将你的组件作为参数,并返回一个包装组件,同时将额外的props注入到这个组件中。
基于以上分析,其实现如下:
// connect() 将一个React组件和一个Redux的store连接起来
import React, { Component } from "react";
import ReduxContext from './index';
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
constructor(props) {
super(props);
this.state = props.store.getState(); // 获取当前 state 树
}
// 当组件加载完成后需要更新state
componentDidMount() {
this.updateComponent(); // 更新state
this.props.store.subscribe(() => this.updateComponent()); // 添加监听器
}
updateComponent() {
const { store } = this.props;
let stateProps = mapStateToProps(store.getState()); // 作为基于 Redux 存储状态的连接组件的 props
let dispatchProps = mapDispatchToProps(store.dispatch); // 一组作为props传递给连接组件的 action creator 函数
// 每次store数据更新后重新渲染一下页面
this.setState({ allProps: { ...stateProps, ...dispatchProps } });
}
render() {
return (
<WrappedComponent {...this.state.allProps} />
);
}
}
// 返回一个包装函数,这个包装函数将你的组件作为参数,并返回一个包装组件,同时将额外的props注入到这个组件中
return (props) => (
<ReduxContext.Consumer>
{(store) => <Connect {...props} store={store} />}
</ReduxContext.Consumer>
);
};
export default connect;
Hooks API
根据官方文档的建议,connect
函数虽然仍然能在React-Redux 8.x版本中使用,但是他们仍建议使用Hooks API作为默认的开发方式。本文目前只实现connect
函数,hooks可以作为后续研究点实现。
4. Provider函数
参考React-Redux官网API文档:Provider
Provider
实际上是一个函数组件,它让任何内嵌的组件都能获取Redux store中的state。通常的做法是在最高层级使用Provider,然后将整个App的组件树放在里面。
其实现如下:
import React from 'react';
import ReduxContext from './index';
const Provider = ({ store, children }) => (
<ReduxContext.Provider value={store}>
{children}
</ReduxContext.Provider>
);
export default Provider;
它借助了ReduxContext
,也就是React.createContext()
。redux-lib
中的index.js
定义如下:
import createStore from "./createStore";
import combineReducers from "./combineReducers";
import connect from "./connect";
import Provider from "./provider";
import React from "react";
const ReduxContext = React.createContext("redux");
export default ReduxContext;
export { createStore, combineReducers, connect, Provider };
其中,ReduxContext
的Provider
组件作为父节点,在App的顶层的index.js
中使用,然后在connect.js
中返回的包装组件使用了ReduxContext
的Consumer
组件,作为子节点,包装了Counter组件然后封装到App中。这时App也可以被看作为一个消费者(子节点)。那么当 Provider 的 value 发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都都不受 shouldComponentUpdate 函数的限制,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
尾声
整个Redux的实现可以在这里找到: HaibiPeng/what-about-react
虽然也是拼拼凑凑,东改西改,但还是希望这篇总结能够帮你更好地理解Redux。
后续还可以有补充的点,比如:
- 实现hooks API功能,代替connect函数
- 加入createStore函数的enhancer参数支持
- 加入connect函数的mergeProps/options参数支持
- ...
另外Redux还有一个很重要的东西就是中间件(middlewares),如支持异步数据流的redux-thunk和redux-saga。之后可能会写一篇关于对中间件的理解。
感谢阅读!下一篇见!