从 Redux 说起,到手写,再到状态管理
学习一个东西之前,首先在大脑中积累充分的“疑惑感”。即弄清面临的问题到底是什么,再浏览方法本身之前,最好先使劲问问都想到什么方法。一个公认的事实是,你对问题的疑惑越大,在之前做的自己的思考越多,当看到解答之后印象就越深刻
先说结论
- Redux 是状态管理库,也是一个架构
- Redux 与 React 无关,但它是为了解决 React 组件中状态无法共享而出的一种解决方案
- 单纯的 Redux 只是一个状态机, store 中存放了所有的状态 state,要想改变里面的状态 state,只能 dispatch 一个动作
- 发出去的 action 需要用 reducer 来处理,传入 state 和 action,返回新的 state
- subscribe 方法可以注册回调方法,当 dispatch action 的时候会执行里面的回调
- Redux 其实是一个发布订阅模式
- Redux 支持 enhancer,enhancer 其实就是一个装饰器函数,传入当前的 createStore,返回一个增强的 createStore
- Redux 使用 applyMiddleware 函数支持中间件,它的返回值其实就是一个 enhancer
- Redux 的中间件也是一个装饰器模式,传入当前的 dispatch,返回一个增强了的 dispatch
- 单纯的 Redux 是没有 View 层的
为什么出现 Redux?
我们默认使用 React 技术栈,当页面少且简单时,完全没必要使用 Redux。Redux 的出现,是为了应对复杂组件的情况。即当组件复杂到三层甚至四层时(如下图),组件 4 想改变组件 1 的状态
按照 React 的做法,状态提升,将状态提升至同一父组件(在图中为祖父组件)。但层级一多,根组件要管理的 state 就很多了,不方便管理。
所以当初有了 context(React 0.14 确定引入),通过 context 能实现”远房组件“的数据共享。但它也有缺点,使用 context 意味着所有的组件都可以修改 context 里面的状态,就像谁都可以修改共享状态一样,导致程序运行的不可预测,这不是我们想要的
meta 提出了 Flux 解决方案,它引入了单向数据流的概念(没错,React 没有单向数据流的概念,Redux 是集成了 Flux 的单向数据流理念),架构如下图所示:
这里不表 Flux。简单理解,在 Flux 架构中,View 要通过 Action (动作)通知 Dispatcher(派发器),Dispatcher 来修改 Store,Store 再修改 View
Flux 的问题或者说缺点在哪?
store 之间存在依赖关系、难以进行服务器端渲染、 store 混杂了逻辑和状态
笔者在学习的 React 技术栈时是 2018 年,那是已然流行 React + Redux 的解决方案,Flux 已经被淘汰了,了解 Flux 是为了引出 Redux
Redux 的出现
Redux 主要解决状态共享问题
官网:Redux 是 JavaScript 状态容器,它提供可预测的状态管理
它的作者是 Dan Abramov,而后成为 React 主开发者,23年离职
其架构为:
可以看得出,Redux 只是一个状态机,没有 View 层。其过程可以这样描述:
- 自己写一个 reducer(纯函数,表示做什么动作会返回什么数据)
- 自己写一个 initState(store 初始值,可写可不写)
- 通过 createStore 生成 store,此变量包含了三个重要的属性
- store.getState:得到唯一值(使用了闭包老哥)
- store.dispatch:动作行为(改变 store 中数据的唯一指定属性)
- store.subscribe:订阅(发布订阅模式)
- 通过 store.dispatch 派发一个 action
- reducer 处理 action 返回一个新的 store
- 如果你订阅过,当数据改变时,你会收到通知
按照行为过程,我们可手写一个 Redux,下文在表,先说特点
三大原则
- 单一数据源
- 整个应用的 全局 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中
- State 是只读的
- 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生时间的普通对象
- 使用纯函数来执行修改
- 为了描述 action 如何改变 state tree,你需要编写纯的 reducers
三大原则是为了更好地开发,按照单向数据流的理念,行为变得可回溯
让我们动手写一个 Redux 吧
手写 redux
按照行为过程和原则,我们要避免数据的随意修改、行为的可回溯等问题
基础版:23 行代码让你使用 redux
export const createStore = (reducer, initState) => {
let state = initState;
let listeners = [];
const subscribe = (fn) => {
listeners.push(fn);
};
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((fn) => fn());
};
const getState = () => {
return state;
};
return {
getState,
dispatch,
subscribe,
};
};
搞个测试用例
import { createStore } from '../redux/index.js';
const initState = {
count: 0,
};
const 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;
}
};
const store = createStore(reducer, initState);
store.subscribe(() => {
let state = store.getState();
console.log('state', state);
});
store.dispatch({
type: 'INCREMENT',
});
PS:俺是在 node 中使用 ES6 模块,需要升级 Node 版本至 13.2.0
第二版:难点突破:中间件
普通的 Redux 只能做最基础地根据动作返回数据,dispatch 只是一个取数据的命令,例如:
dispatch({
type: 'INCREMENT',
});
// store 中的 count + 1
但在开发中,我们有时候要查看日志、异步调用、记录日常等
怎么办,做成插件
在 Redux 中,类似的概念叫中间件
Redux 的 createStore 共有三个参数
createStore([reducer], [initial state], [enhancer]);
第三个参数为 enhancer,意为增强器。它的作用就是代替普通的 createStore,转变成为附加上中间件的 createStore。打几个比方:
- 托尼·斯塔克本来是一个普通有钱人,加上增强器(盔甲)后,成了钢铁侠
- 中央下发一笔救灾款,加上增强器(大小官员的打点)后,到灾民手上的钱只有一丢丢
- 路飞用武装色打人,武装色就是一个中间件
enhancer 要做的就是:东西还是那个东西,只是经过了一些工序,加强了它。这些工序由 applyMiddleware 函数完成。按照行业术语,它是一个装饰器模式。它的写法大致是:
applyMiddleware(...middlewares);
// 结合 createStore,就是
const store = createStore(reducer, initState, applyMiddleware(...middlewares));
所以我们需要先对 createStore 进行改造,判断当有 enhancer 时,我们需传值给中间件
export const createStore = (reducer, initState, enhancer) => {
if (enhancer) {
const newCreateStore = enhancer(createStore)
return newCreateStore(reducer, initState)
}
let state = initState;
let listeners = [];
...
}
如果有 enhancer 的话,先传入 createStore 函数,生成的 newCreateStore 和原来的 createStore 一样,会根据 reducer, initState 生成 store。可简化为:
if (enhancer) {
return enhancer(createStore)(reducer, initState);
}
PS:为什么要写成这样,因为 redux 是按照函数式写法来写的
为什么 createStore 可以被传值,因为函数也是对象,也可以作为参数传递(老铁闭包了)
这样我们的 applyMiddleware 自然就明确了
const applyMiddleware = (...middlewares) => {
return (oldCreateStore) => {
return (reducer, initState) => {
const store = oldCreateStore(reducer, initState)
...
}
}
}
这里的 store 表示的是普通版中的 store,接下来我们要增强 store 中的属性
我愿称之为:五行代码让女人为我花了 18 万
export const applyMiddleware = (...middlewares) => {
return (oldCreateStore) => {
return (reducer, initState) => {
const store = oldCreateStore(reducer, initState);
// 以下为新增
const chain = middlewares.map((middleware) => middleware(store));
// 获得老 dispatch
let dispatch = store.dispatch;
chain.reverse().map((middleware) => {
// 给每个中间件传入原派发器,赋值中间件改造后的dispatch
dispatch = middleware(dispatch);
});
// 赋值给 store 上的 dispatch
store.dispatch = dispatch;
return store;
};
};
};
现在写几个中间件来测试一下
// 记录日志
export const loggerMiddleware = (store) => (next) => (action) => {
console.log('this.state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
};
// 记录异常
export const exceptionMiddleware = (store) => (next) => (action) => {
try {
next(action);
} catch (error) {
console.log('错误报告', error);
}
};
// 时间戳
export const timeMiddleware = (store) => (next) => (action) => {
console.log('time', new Date().getTime());
next(action);
};
引入项目中,并运行
import { createStore, applyMiddleware } from '../redux/index.js';
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js';
const initState = {
count: 0,
};
const 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;
}
};
const store = createStore(
reducer,
initState,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
);
store.subscribe(() => {
let state = store.getState();
console.log('state', state);
});
store.dispatch({
type: 'INCREMENT',
});
运行发现已经实现了 redux 最重要的功能——中间件
来分析下中间件的函数式编程,以 loggerMiddleware 为例:
export const loggerMiddleware = (store) => (next) => (action) => {
console.log('this.state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
};
在 applyMiddleware 源码中,
const chain = middlewares.map((middleware) => middleware(store));
相当于给每个中间件传值普通版的 store
let dispatch = store.dispatch;
chain.reverse().map((middleware) => (dispatch = middleware(dispatch)));
相当于给每个中间件在传入 store.dispatch,也就是 next,原 dispatch = next。这个时候的中间件已经本成品了,代码中的 (action) => {...}
就是函数 const dispatch = (action) => {}
。当你执行 dispatch({ type: XXX })
时执行中间件这段(action) => {...}
PS:柯里化一开始比较难理解,用多习惯就慢慢能懂
第三版:结构复杂化与拆分
中间件理解起来或许有些复杂,先看看其他的概念换换思路
一个应用做大后,单靠一个 JavaScript 文件来维护代码显然是不科学的,在 Redux 中,为避免这类情况,它提供了 combineReducers
来整个多个 reducer,使用方法如:
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
});
在 combinReducers
中传入一个对象,什么样的 state 对应什么样的 reducer。这就好了,那么 combinReducers
怎么实现呢?因为比较简单,不做多分析,直接上源码:
export const combinReducers = (...reducers) => {
// 拿到 counter、info
const reducerKey = Object.keys(reducers);
// combinReducers 合并的是 reducer,返回的还是一个 reducer,所以返回一样的传参
return (state = {}, action) => {
const nextState = {};
// 循环 reducerKey,什么样的 state 对应什么样的 reducer
for (let i = 0; i < reducerKey.length; i++) {
const key = reducerKey[i];
const reducer = reducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
}
return nextState;
};
};
同级目录下新建一个 reducer 文件夹,并新建 reducer.js
、info.js
、index.js
// reducer.js
export default (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
};
case 'DECREMENT': {
return {
count: state.count - 1,
};
}
default:
return state;
}
};
// info.js
export default (state, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name,
};
case 'SET_DESCRIPTION':
return {
...state,
description: action.description,
};
default:
return state;
}
};
合并导出
import counterReducer from './counter.js';
import infoReducer from './info.js';
export { counterReducer, infoReducer };
我们现在测试一下
import {
createStore,
applyMiddleware,
combinReducers,
} from '../redux/index.js';
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js';
import { counterReducer, infoReducer } from './reducer/index.js';
const initState = {
counter: {
count: 0,
},
info: {
name: 'johan',
description: '前端之虎',
},
};
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
});
const store = createStore(
reducer,
initState,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
);
store.dispatch({
type: 'INCREMENT',
});
combinReducers
也完成了
既然拆分了 reducer,那么 state 是否也能拆分,并且它是否需要传,在我们平时的写法中,一般都不传 state。这里需要两点改造,一是每个 reducer 中包含了它的 state 和 reducer;二是改造 createStore,让 initState 变得可传可不传,以及初始化数据
// counter.js 中写入对应的 state 和 reducer
let initState = {
counter: {
count: 0,
},
};
export default (state, action) => {
if (!state) {
state = initState;
}
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
};
case 'DECREMENT': {
return {
count: state.count - 1,
};
}
default:
return state;
}
};
// info.js
let initState = {
info: {
name: 'johan',
description: '前端之虎',
},
};
export default (state, action) => {
if (!state) {
state = initState;
}
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name,
};
case 'SET_DESCRIPTION':
return {
...state,
description: action.description,
};
default:
return state;
}
};
改造 createStore
export const createStore = (reducer, initState, enhancer) => {
if (typeof initState === 'function') {
enhancer = initState;
initState = undefined
}
...
const getState = () => {
return state
}
// 用一个不匹配任何动作来初始化store
dispatch({ type: Symbol() })
return {
getState,
dispatch,
subscribe
}
}
主文件中
import { createStore, applyMiddleware, combinReducers } from './redux/index.js';
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js';
import { counterReducer, infoReducer } from './reducer/index.js';
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
});
const store = createStore(
reducer,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
);
console.dir(store.getState());
到此为止,我们已经实现了一个七七八八的 redux 了
完整体的 Redux
退订
const subscribe = (fn) => {
listeners.push(fn);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
};
中间件拿到的 store
现在的中间件能拿到完整的 store,他甚至可以修改我们的 subscribe 方法。按照最小开放策略,我们只用给 getState 即可,修改下 applyMiddleware 中给中间件传的 store
// const chain = middlewares.map(middleware => middleware(store))
const simpleStore = { getState: store.getState };
const chain = middlewares.map((middleware) => middleware(simpleStore));
compose
在我们的 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 compose = (...funcs) => {
if (funcs.length === 0) {
return (args) => args;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args)),
);
};
2 行代码 replaceReducer
替换当前的 reudcer ,使用场景:
- 代码分割
- 动态加载
- 实时 reloading 机制
const replaceReducer = (nextReducer) => {
reducer = nextReducer;
// 刷新一次,广播 reducer 已经替换,也同样把默认值换成新的 reducer
dispatch({ type: Symbol() });
};
bindActionCreators
bindActionCreators 是做什么的,他通过闭包,把 dispatch 和 actionCreator 隐藏起来,让其他地方感知不到 redux 的存在。一般与 react-redux 的 connect 结合
这里直接贴源码实现:
const bindActionCreator = (actionCreator, dispatch) => {
return function () {
return dispatch(actionCreator.apply(this, arguments));
};
};
export const bindActionCreators = (actionCreators, dispatch) => {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch);
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error();
}
const keys = Object.keys(actionCreators);
const boundActionCreators = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const actionCreator = actionCreators[key];
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(
actionCreator,
dispatch,
);
}
}
return boundActionCreators;
};
以上,我们就已经完成了 Redux 中所有的代码。大体上这里 100 多行的代码就是 Redux 的全部,真 Redux 无非是加了些注释和参数校验
总结
我们把与 Redux 相关的名词列出来,梳理它是做什么的
- createStore
- 创建 store 对象,包含 getState、dispatch、subscribe、replaceReducer
- reducer
- 纯函数,接受旧的 state、action,生成新的 state
- action
- 动作,是一个对象,必须包括 type 字段,表示 view 发出通知告诉 store 要改变
- dispatch
- 派发,触发 action ,生成新的 state。是 view 发出 action 的唯一方法
- subscribe
- 订阅,只有订阅了,当派发时,会执行订阅函数
- combineReducers
- 合并 reducer 成一个 reducer
- replaceReudcer
- 代替 reducer 的函数
- middleware
- 中间件,扩展 dispatch 函数
砖家曾经画过一张关于 Redux 的流程图
换种思考方式理解
我们说过, Redux 只是一个状态管理库,它是由数据来驱动,发起 action,会引发 reducer 的数据更新,从而更新到最新的 store
与 React 结合
拿着刚做好的 Redux,放到 React 中,试试什么叫 Redux + React 集合,注意,这里我们先不使用 React-Redux,单拿这两个结合
先创建项目
npx create-react-app demo-5-react
引入手写的 redux 库
在 App.js
中引入 createStore,并写好初始数据和 reducer,在 useEffect 中监听数据,监听好之后当发起一个 action 时,数据就会改变,看代码:
import React, { useEffect, useState } from 'react';
import { createStore } from './redux';
import './App.css';
const initState = {
count: 0,
};
const 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;
}
};
const store = createStore(reducer, initState);
function App() {
const [count, setCount] = useState(store.getState().count);
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setCount(store.getState().count);
});
return () => {
if (unsubscribe) {
unsubscribe();
}
};
}, []);
const onHandle = () => {
store.dispatch({
type: 'INCREMENT',
});
console.log('store', store.getState().count);
};
return (
<div className="App">
<div>{count}</div>
<button onClick={onHandle}>add</button>
</div>
);
}
export default App;
点击 button 后,数据跟着改变
PS:虽然我们可以用这种方式订阅 store 和改变数据,但是订阅的代码重复过多,我们可以用高阶组件将他提取出去。这也是 React-Redux 所做的事情
与原生 JS+HTML 结合
我们说过,Redux 是个独立于 Redux 的存在,它不仅可在 Redux 充当数据管理器,还可以在原生 JS + HTML 中充当起职位
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="container">
<div id="count">1</div>
<button id="btn">add</button>
</div>
<script type="module">
import { createStore } from './redux/index.js';
const initState = {
count: 0,
};
const 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;
}
};
const store = createStore(reducer, initState);
let count = document.getElementById('count');
let add = document.getElementById('btn');
add.onclick = function () {
store.dispatch({
type: 'INCREMENT',
});
};
// 渲染视图
function render() {
count.innerHTML = store.getState().count;
}
render();
// 监听数据
store.subscribe(() => {
let state = store.getState();
console.log('state', state);
render();
});
</script>
</body>
</html>
效果如下:
状态生态
我们从 Flux 说到 Redux,再从 Redux 说了各种中间件,其中 React-saga 就是为解决异步行为而生的中间件,它主要采用 Generator(生成器)概念,比起 React-thunk 和 React-promise,它没有像其他两者将异步行为放在 action creator 上,而是把所有的异步操作看成“线程”,通过 action 触发它,当操作完成后再次发出 action 作为输出
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
yield 'ending';
}
const helloWorld = helloWorldGenerator();
hewlloWorld.next(); // { value: 'hello', done: false }
hewlloWorld.next(); // { value: 'world', done: false }
hewlloWorld.next(); // { value: 'ending', done: true }
hewlloWorld.next(); // { value: undefined, done: true }
简单来说:遇到 yield 表达式,就暂停执行后面的操作,并将紧跟 yield 后面的那个表达式的值,作为返回值 value,等着下一个调用 next 方法,再继续往下执行
Dva
Dva 是什么?
官网:Dva 首先是一个基于 Redux + Redux-saga 的数据流方案。为了简化开发体验,Dva 还额外内置了 react-router 和 fetch,所以可以理解为一个轻量级的应用框架
简单来说,它是整合了现在最流行的数据流方案,即一个 React 技术栈:
dva = React-Router + Redux + Redux-saga + React-Redux
它的数据流图为:
view dispatch 一个动作,改变 state(即 store),state 与 view 绑定,响应 view
其他不表,可去 Dva 官网查看,这里讲讲 Model ,它包含了 5 个属性
- namespace
- model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过
.
的方式创建多层命名空间
- model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过
- state
- 初始值
- reducers
- 纯函数,以 key/value 格式定义 reducer。用于处理同步擦做,唯一可以修改
state
的地方,由action
触发 - 格式为:
(state, action) => newState
或[(state, action) => newState, enhancer]
- 纯函数,以 key/value 格式定义 reducer。用于处理同步擦做,唯一可以修改
- effects
- 处理异步操作和业务逻辑,以 key/value 格式定义 effect
- 不直接修改 state。由 action 触发
- call:执行异步操作
- put:发出一个 Action,类似于 dispatch
- subscriptions
- 订阅
- 在
app.start()
时被执行,数据源可以是当前的时间、服务器的 websocket 链接、 keyboard 输入、history 路由变化、geolocation 变化等等
Mobx
View 通过订阅也好,监听也好,不同的框架有不同的技术,总之 store 变化, view 也跟着变化
Mobx 使用的是响应式数据流方案。后续会单独写一篇,此篇太长,先不写
补充:单向数据流
先介绍 React 中数据传递,即通信问题
- 向子组件发消息
- 向父组件发消息
- 向其他组件发消息
React 只提供了一种通信方式:传参。
即父传值给子,子不能修改父传的数据,props 具有不可修改性。子组件想把数据传给父组件怎么办?通过 props 中的事件来传值通知父组件