title | date |
---|---|
redux 原理以及实现 |
2020-03-02 08:32:28 -0800 |
本文从零实现一个简单的 redux
和 react-redux
,主要内容在于
redux
的设计思路及实现原理redux 中间件
的设计思路及实现原理react-redux
的设计思路及实现原理
redux
是一个状态管理器,里面存放着数据,比如我们创建 store.js
,在里面我们存放着这些数据,只需要在任何地方引用这个文件就可以拿到对应的状态值:
let state = {
count: 1,
};
我们读取和修改下状态:
console.log(state.count);
state.count = 2;
现在我们实现了状态(计数)的修改和使用了!当然上面的有一个很明显的问题:
- 这个状态管理器只能管理
count
,不通用. - 修改
count
之后,使用count
的地方不能收到通知。
我们可以使用发布-订阅模式来解决这个问题。我们用个函数封装一下这个 redux
function createStore(initState) {
let state = initState;
let listeners = [];
/* 订阅函数 */
function subscribe(listener) {
listeners.push(listener);
}
function changeState(newState) {
state = newState;
/* 执行通知 */
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return { subscribe, changeState, getState };
}
到这里我们完成了一个简单的状态管理器。
state
的数据可以自由的定- 我们修改状态,在订阅的地方监听变化,可以实现监听。
let initState = {
count: 1,
info: {
age: 18,
},
};
let store = createStore(initState);
store.subscribe(() => {
let state = store.getState();
console.log('subscribe function one: ', state);
});
store.subscribe(() => {
let state = store.getState();
console.log('subscribe function two: ', state);
});
store.changeState({ ...store.getState(), count: store.getState().count + 1 });
store.changeState({
...store.getState(),
info: { age: store.getState().info.age - 1 },
});
// ==== result
// subscribe function one: { count: 2, info: { age: 18 } }
// subscribe function two: { count: 2, info: { age: 18 } }
// subscribe function one: { count: 2, info: { age: 17 } }
// subscribe function two: { count: 2, info: { age: 17 } }
这里需要理解的是 createStore
,提供了 changeState
,getState
,subscribe
三个能力。
在上面的函数中,我们调用 store.changeState
可以改变 state
的值,这样就存在很大的弊端了。比如 store.changeState({})
我们一不小心就会把
store
的数据清空,或者误修改了其他组件的数据,那显然不太安全,出错了也很难排查,因此我们需要有条件地操作store
,防止使用者直接修改store
的数据。
因此,我们需要一个约束来修改 state
的值,而不允许意外的情况来将 state
的值清空或者误操作。我们可以分两步来解决这个问题:
dispatch
: 制定一个state
修改计划,告诉store
,我的修改计划是什么。reducer
: 修改store.changeState
方法,告诉它修改state
的时候,按照我们的计划修改。
也即,我们将 store.changeState
改写为 store.dispatch
, 在函数中传递多一个 reducer
函数来约束状态值的修改。
reducer 是一个纯函数,接受一个 state, 返回新的 state。
function createStore(reducer, initState) {
let state = initState;
let listeners = [];
/* 订阅函数 */
function subscribe(listener) {
listeners.push(listener);
}
/* state 值的修改 */
function dispatch(action) {
state = reducer(state, action);
/* 执行通知 */
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return { subscribe, dispatch, getState };
}
我们来尝试使用 dispatch
和 reducer
来实现自增和自减
let initState = {
count: 1,
info: {
age: 18,
},
};
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
let store = createStore(reducer, initState);
store.subscribe(() => {
let state = store.getState();
console.log('subscribe function: ', state);
});
store.dispatch({ type: 'INCREMENT' }); // 自增
store.dispatch({ type: 'DECREMENT' }); // 自减
store.dispatch({ count: 2 }); // 计划外:不生效
我们知道 reducer
是一个约束函数,接收老的 state
,按计划返回新的 state
。那我们项目中,有大量的 state
,每个 state
都需要约束函数,如果全部写在一起会是啥样子呢?
所有的计划写在一个 reducer
函数里面,会导致 reducer
函数及其庞大复杂。下面将封装 combineReducers
来颗粒化 reducer
函数。
按经验来说,我们肯定会按组件维度来拆分出很多个 reducer
函数,然后通过一个函数来把他们合并起来。
我们来管理两个 state
,一个 counter
,一个 info
。
let state = {
counter: { count: 0 },
info: { age: 18 },
};
他们各自的 reducer
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
}
function infoReducer(state, action) {
switch (action.type) {
case 'INCREMENT-AGE':
return { age: state.age + 1 };
default:
return state;
}
}
我们尝试实现下 combineReducers
函数
- 传入对象参数,
key
值即为state
状态树的key
值,value
为对应的reducer
函数。 - 遍历对象参数,执行每一个
reducer
函数,传入state[key]
, 函数获得每个reducer
最新的state
值。 - 耦合
state
的值, 并返回。返回合并后的新的reducer
函数。
function combineReducers(reducers) {
/* reducerKeys = ['counter', 'info']*/
const reducerKeys = Object.keys(reducers);
/*返回合并后的新的reducer函数*/
return function combination(state = {}, action) {
/*生成的新的state*/
const nextState = {};
/*遍历执行所有的reducers,整合成为一个新的state*/
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
const reducer = reducers[key];
/*之前的 key 的 state*/
const previousStateForKey = state[key];
/*执行 分 reducer,获得新的state*/
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
}
return nextState;
};
}
使用 combineReducers
:
const reducers = combineReducers({
counter: counterReducer,
info: infoReducer,
});
let store = createStore(reducers, initState);
store.subscribe(() => {
let state = store.getState();
console.log('subscribe function: ', state);
});
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT-AGE' });
然而这还不够,我们把 reducer
按组件维度拆分了,通过 combineReducers
合并了起来。 但是还有个问题, state
我们还是写在一起的,这样会造成 state
树很庞大,不直观,很难维护。我们需要拆分,一个 state
,一个 reducer
写一块。
改写 combineReducers
函数,无非很简单,在 createStore
函数中执行 dispatch({ type: Symbol() })
function createStore(reducer, initState) {
let state = initState;
let listeners = [];
/* 订阅函数 */
function subscribe(listener) {
listeners.push(listener);
}
function dispatch(action) {
state = reducer(state, action);
/* 执行通知 */
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
/* 注意!!!只修改了这里,用一个不匹配任何计划的 type,来获取初始值 */
dispatch({ type: Symbol() });
function getState() {
return state;
}
return { subscribe, dispatch, getState };
}
将 state
分别传入各自的 reducer
:
function counterReducer(state = { count: 1 }, action) {
//...
}
function infoReducer(state = { age: 18 }, action) {
//...
}
// 合并 reducer
const reducers = combineReducers({
counter: counterReducer,
info: infoReducer,
});
// 移除 initState
let store = createStore(reducers);
console.log(store.getState()); // { counter: { count: 1 }, info: { age: 18 } }
我们思考下这行可以带来什么效果?
createStore
的时候,用一个不匹配任何type
的action
,来触发state = reducer(state, action)
- 因为
action.type
不匹配,每个子reducer
都会进到default
项,返回自己初始化的state
,这样就获得了初始化的state
树了。
如果您使用过 Express
和 Koa
之类的服务器端库,那么您可能已经很熟悉中间件的概念。
所谓中间件,我们可以理解为拦截器,用于对某些过程进行拦截和处理,且中间件之间能够串联使用。这里主要是对 dispatch
的扩展,或者说重写,增强 dispatch
的功能。
首先让我们实现 redux-logger
插件,即打印日志功能,记录修改前后的 state
和 action
,我们可以通过重写 store.dispatch
来实现:
const reducers = combineReducers({ counter: counterReducer });
let store = createStore(reducers);
const next = store.dispatch;
// 重写 dispatch
store.dispatch = action => {
console.log('prevState: ', store.getState());
console.log('action', action);
next(action);
console.log('nextState: ', store.getState());
};
store.dispatch({ type: 'INCREMENT' });
输出结果
prevState: { counter: { count: 1 } }
action { type: 'INCREMENT' }
nextState: { counter: { count: 2 } }
现在我们已经实现了一个简单的 redux-logger
中间件了。我又有一个需求,需要记录每次数据出错的原因,我们扩展下 dispatch
store.dispatch = action => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err);
}
};
这样每次 dispatch 出异常的时候,我们都会记录下来。
我现在既需要记录日志,又需要记录异常,怎么办?当然很简单了,两个函数合起来呗!
store.dispatch = action => {
try {
console.log('prevState: ', store.getState());
console.log('action', action);
next(action);
console.log('nextState: ', store.getState());
} catch (err) {
console.error('错误报告: ', err);
}
};
如果又来一个需求怎么办?接着改 dispatch
函数?那再来 10 个需求呢?到时候 dispatch
函数肯定庞大混乱到无法维护了!
我们需要考虑如何实现扩展性很强的多中间件合作模式。
- 我们把
loggerMiddleware
提取出来
const loggerMiddleware = action => {
console.log('prevState: ', store.getState());
console.log('action', action);
next(action);
console.log('nextState: ', store.getState());
};
- 我们把
exceptionMiddleware
提取出来
const exceptionMiddleware = action => {
try {
/*next(action)*/
loggerMiddleware(action);
} catch (err) {
console.error('错误报告: ', err);
}
};
store.dispatch = exceptionMiddleware;
- 现在的代码有一个很严重的问题,就是
exceptionMiddleware
里面写死了loggerMiddleware
,我们需要让next(action)
变成动态的,随便哪个中间件都可以
const exceptionMiddleware = next => action => {
try {
/*loggerMiddleware(action);*/
next(action);
} catch (err) {
console.error('错误报告: ', err);
}
};
/*loggerMiddleware 变成参数传进去*/
store.dispatch = exceptionMiddleware(loggerMiddleware);
- 同样的道理,
loggerMiddleware
里面的next
现在恒等于store.dispatch
,导致loggerMiddleware
里面无法扩展别的中间件了!我们也把next
写成动态的
const loggerMiddleware = next => action => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
};
到这里为止,我们已经探索出了一个扩展性很高的中间件合作模式!
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = next => action => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
};
const exceptionMiddleware = next => action => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err);
}
};
store.dispatch = exceptionMiddleware(loggerMiddleware(next));
这时候我们开开心心的新建了一个 loggerMiddleware.js
,一个 exceptionMiddleware.js
文件,想把两个中间件独立到单独的文件中去。会碰到什么问题吗?
loggerMiddleware
中包含了外部变量 store
,导致我们无法把中间件独立出去。那我们把 store
也作为一个参数传进去好了~
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = store => next => action => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
};
const exceptionMiddleware = store => next => action => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err);
}
};
const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));
到这里为止,我们真正的实现了两个可以独立的中间件啦!
现在我有一个需求,在打印日志之前输出当前的时间戳。用中间件来实现!
const timeMiddleware = store => next => action => {
console.log('time', new Date().getTime());
next(action);
};
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));
上一节我们已经完全实现了正确的中间件!但是中间件的使用方式不是很友好
let store = createStore(reducers);
const next = store.dispatch;
const loggerMiddleware = store => next => action => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
};
const exceptionMiddleware = store => next => action => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err);
}
};
const timeMiddleware = store => next => action => {
console.log('time', new Date().getTime());
next(action);
};
const time = timeMiddleware(store);
const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(time(logger(next)));
其实我们只需要知道三个中间件,剩下的细节都可以封装起来!我们通过扩展 createStore
来实现!
先来看看期望的用法
/*接收旧的 createStore,返回新的 createStore*/
const newCreateStore = applyMiddleware(
exceptionMiddleware,
timeMiddleware,
loggerMiddleware,
)(createStore);
/*返回了一个 dispatch 被重写过的 store*/
const store = newCreateStore(reducer);
实现 applyMiddleware
const applyMiddleware = function(...middlewares) {
/*返回一个重写createStore的方法*/
return function rewriteCreateStoreFunc(oldCreateStore) {
/*返回重写后新的 createStore*/
return function newCreateStore(reducer, initState) {
/*1. 生成store*/
const store = oldCreateStore(reducer, initState);
/*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
/* const chain = [exception, time, logger]*/
const chain = middlewares.map(middleware => middleware(store));
let dispatch = store.dispatch;
/* 实现 exception(time((logger(dispatch))))*/
chain.reverse().map(middleware => {
dispatch = middleware(dispatch);
});
/*2. 重写 dispatch*/
store.dispatch = dispatch;
return store;
};
};
};
现在还有个小问题,我们有两种 createStore
了。
/*没有中间件的 createStore*/
let store = createStore(reducers, initState);
/*有中间件的 createStore*/
const rewriteCreateStoreFunc = applyMiddleware(
exceptionMiddleware,
timeMiddleware,
loggerMiddleware,
);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(reducer, initState);
为了让用户用起来统一一些,我们可以很简单的使他们的使用方式一致,我们修改下 createStore
方法
function createStore(reducer, initState, rewriteCreateStoreFunc) {
/*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
if (rewriteCreateStoreFunc) {
const newCreateStore = rewriteCreateStoreFunc(createStore);
return newCreateStore(reducer, initState);
}
/*否则按照正常的流程走*/
//...
}
最终的用法
const rewriteCreateStoreFunc = applyMiddleware(
exceptionMiddleware,
timeMiddleware,
loggerMiddleware,
);
const store = createStore(reducer, initState, rewriteCreateStoreFunc);
我们的 applyMiddleware
中,把 [A, B, C]
转换成 A(B(C(next)))
,是这样实现的
const chain = [A, B, C];
let dispatch = store.dispatch;
chain.reverse().map(middleware => {
dispatch = middleware(dispatch);
});
redux
提供了一个 compose
方式,可以帮我们做这个事情
const chain = [A, B, C];
dispatch = compose(...chain)(store.dispatch);
看下他是如何实现的
export default function compose(...funcs) {
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
当然 compose
函数对于新人来说可能比较难理解,你只需要他是做什么的就行啦!
有时候我们创建 store
的时候不传 initState
,我们怎么用?
const store = createStore(reducer, {}, rewriteCreateStoreFunc);
redux
允许我们这样写
const store = createStore(reducer, rewriteCreateStoreFunc);
我们仅需要改下 createStore
函数,如果第二个参数是一个 objec
t,我们认为他是 initState
,如果是 function
,我们就认为他是 rewriteCreateStoreFunc
。
function craeteStore(reducer, initState, rewriteCreateStoreFunc) {
if (typeof initState === 'function') {
rewriteCreateStoreFunc = initState;
initState = undefined;
}
//...
}
在上文,我们完成了一个简单的 redux
。
一个组件如果想从 store 存取公用状态,需要进行四步操作:import
引入 store
、getState
获取状态、dispatch
修改状态、subscribe
订阅更新,代码相对冗余,
而 react-redux
就提供了一种合并操作的方案:react-redux
提供 Provider
和 connect
两个 API,Provider
将 store
放进 this.context
里,省去了 import
这一步,connec
t 将 getState
、dispatch
合并进了 this.props
,并自动订阅更新,简化了另外三步,
import React from 'react';
import PropTypes from 'prop-types';
export default class Provider extends React.Component {
// 需要声明静态属性childContextTypes来指定context对象的属性,是context的固定写法
static childContextTypes = {
store: PropTypes.object,
};
// 实现getChildContext方法,返回context对象,也是固定写法
getChildContext() {
return { store: this.store };
}
constructor(props, context) {
super(props, context);
this.store = props.store;
}
// 渲染被Provider包裹的组件
render() {
return this.props.children;
}
}
完成 Provider
后,我们就能在组件中通过 this.context.store
这样的形式取到 store
,不需要再单独 import store
。
下面我们来思考一下如何实现 connect
,我们先回顾一下 connect
的使用方法:
connect(mapStateToProps, mapDispatchToProps)(App);
我们已经知道,connect
接收 mapStateToProps
、mapDispatchToProps
两个方法,然后返回一个高阶函数,这个高阶函数接收一个组件,返回一个高阶组件(其实就是给传入的组件增加一些属性和功能)connect
根据传入的 map
,将 state
和 dispatch(action)
挂载子组件的 props
上,我们直接放出 connect
的实现代码,寥寥几行,并不复杂:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import bindActionCreators from '../redux/bindActionCreators';
export default function connect(mapStateToProps, mapDispatchToProps) {
return function(Component) {
class Connect extends React.Component {
componentDidMount() {
// 从context获取store并订阅更新
this.context.store.subscribe(this.handleStoreChange.bind(this));
}
handleStoreChange() {
// 触发的方法有多种,这里为了简洁起见,直接forceUpdate强制更新,读者也可以通过setState来触发子组件更新
this.forceUpdate();
}
render() {
const dispathProps =
typeof mapDispatchToProps &&
bindActionCreators(mapDispatchToProps, this.context.store.dispatch);
return (
<Component
// 传入该组件的props,需要由connect这个高阶组件原样传回原组件
{...this.props}
// 根据mapStateToProps把state挂到this.props上
{...mapStateToProps(this.context.store.getState())}
// 根据mapDispatchToProps把dispatch(action)挂到this.props上
{...dispathProps}
/>
);
}
}
// 接收context的固定写法
Connect.contextTypes = {
store: PropTypes.object,
};
return Connect;
};
}