❝redux 是前端的状态管理方案之一,经常在
❞React项目中使用。其自身的实现用到了函数式编程和观察者模式,值得深入研究一下。
三大特性
- 全局唯一 store
- state 只读,即数据的不可变性
- reducer 必须是一个纯函数
单向数据流
- 视图层调用
store.dispatch(action)派发一个 action - reducer 以当前 state 和 action 为参数,匹配到对应的 action.type 计算生成一份新的 state 并返回
- 执行
store.subscribe()注册的监听函数,触发视图更新
createStore
- 这个函数用来生成 store,接收三个参数,第一个 reducer 必传,后面的 initialState 和 enhancr 可选,返回的 store 上面有 getState,dispatch 和 subscribe 方法,由于 state 只能通过派发 action 修改,不允许直接访问和修改,因此放到函数内部,基本 API 实现如下:
// redux.js
export function createStore(reducer, initialState, enhancr) {
let currentState = initialState; // 应用的共享状态
let isDispatching = false;
let currentListeners = []; // 注册的监听函数
function getState() {
if (isDispatching) throw New Error('err');
return currentState; // 获取当前的应用状态
}
function dispatch(action) { // 修改当前的应用状态
// 计算生成新的 state
currentState = reducer(currentState, action);
// state 发生改变,调用监听函数触发视图更新,观察者模式
currentListeners.forEach(fn => fn());
}
function subscribe (listener) { // 注册监听函数
if (isDispatching) throw New Error('err');
currentListeners.push(listener);
return function unsubscribe() {}; // subscribe 的返回值是一个取消监听的函数
}
reutrn {
getState,
dispatch,
subscribe,
}
}
- 这样我们在自己的项目中就可以通过 createStore 来创建 store,编写对应的 reducer 来修改状态,例如官方文档中的计数器:
reducer.js
export default function counter(state, action) {
switch(action.type) {
case 'add':
return {
...state,
count: state.count + 1,
}
case '':
return {
...state,
count: state.count - 1,
}
default:
return state;
}
}
// app.js
import reducer from './reducer';
impoer { createStore } from './redux';
const initialState = { count: 0 };
const store = createStore(reducer, initialState);
store.subscribe(() => { console.log('需要更新') }); // 注册监听函数,state 发生变化就自动执行
console.log(store.getState()); // 获取应用当前状态
store.dispatch({ type: 'add' }); // 计数器加一
console.log(store.getState()); // 获取应用当前状态
store.dispatch({ type: 'add' }); // 计数器减一
console.log(store.getState()); // 获取应用当前状态
❝ ❞
中间件
- 可以看到我们已经实现了 redux 的基本功能,但此时它只能处理同步的 action;此外如果我们想添加一些额外功能比如日志记录等该怎么办呢,最简单的方法就是像上面一样手动记录,但这样肯定不行,反正 state 的改变是在 dispatch 的时候发生的,那我们可以尝试改写 dispatch 方法,让它自带日志打印的功能:
const next = store.dispatch; // 原本只能派发 action 的旧 dispatch
store.dispatch = function(action) { // 新改写的具有额外功能的方法
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
- 假如还需要有一个额外的记录错误信息的功能,那该怎么办呢,try catch 包一下:
const next = store.dispatch;
store.dispatch = function(action) {
try {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
} catch(err) {
// 记录 err
}
}
- 但是这样我们相当于把不同的功能耦合到了一起,是无法良好的扩展的,比如再想增加一个派发 action 的时候输出时间戳的功能,就得继续修改 dispatch 函数,所以我们需要考虑实现具有良好扩展的多中间件模式,可以把多个功能单独分离出来:
// 负责日志打印的
const loggerMiddleware = function(action) {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
// 负责错误收集的
const exceptionMiddleware = function(action) {
try {
loggerMiddleware(action)
} catch(err) {
// err 记录
}
}
- 这样在使用的时候就可以先调用错误收集的中间件,传入 action,再在内部调用日志收集的中间件:
const next = store.dispatch;
store.dispatch = exceptionMiddleware;
- 但这样的问题是中间件代码都是写死的,我们需要让中间件的调用是动态的,想调用哪个都可以,所以需要把中间件函数写成一个高阶函数:
// 负责错误收集的
const exceptionMiddleware = function(middleWare) {
return function(action) {
try {
middleWare(action)
} catch(err) {
// err 记录
}
}
}
// 负责日志打印的
const loggerMiddleware = function(middleWare) {
return function(action) {
console.log('this state', store.getState());
middleWare(action);
console.log('this state', store.getState());
}
}
- 由于两层函数是直接 return,所以我们可以使用箭头函数改写,就是喜闻乐见的双箭头函数了:
const exceptionMiddleware = middleWare => action => {
try {
middleWare(action)
} catch(err) {
// err 记录
}
}
const loggerMiddleware = middleWare => action => {
console.log('this state', store.getState());
middleWare(action);
console.log('this state', store.getState());
}
- 此时我们已经完成了一个扩展性良好的中间件模式,这样在调用的时候就应该是:
const next = store.dispatch;
store.dispatch = exceptionMiddleware(loggerMiddleware(next));
- 但中间件肯定是要独立出去的,而 store 是我们自己生成的,也就是说中间件内部不能直接包含 store,这个变量也应该是执行时传入的,所以应该再加一层:
const exceptionMiddleware = store => middleWare => action => {
try {
middleWare(action)
} catch(err) {
// err 记录
}
}
const loggerMiddleware = store => middleWare => action => {
console.log('this state', store.getState());
middleWare(action);
console.log('this state', store.getState());
}
const store = createStore(reducer);
const next = store.dispatch;
const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));
- 现在,真正独立可扩展的中间件就完成了,比如现在需要加一个输出时间戳的功能,就可以:
...
const timeMiddleware = store => middleWare => action => {
console.log('time', Date.now());
middleWare(action);
}
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));
applyMiddleWare
- 在 redux 中我们已经有了 store,其实只需要知道准备使用哪些中间件就可以,其他细节都可以封装到 redux 自身的实现中,并且这种依次嵌套的调用方式不那么友好,反正实现的功能就是若干个函数(中间件)依次调用且一个函数的输出结果是下一个函数的输入,因此可以采用 compose 的方式去组合函数:
// 之前的调用方式
const store = createStore(reducer);
const next = store.dispatch;
...
store.dispatch = exception(time(logger(next)));
// 期待的调用方式
const newCreateStore = applyMiddleWare(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore)
// 生成一个 dispatch 方法被重写(增强)后的,有了额外功能 store
const store = newCreateStore(reducer);
第一版 applyMiddleWare
/**
* applyMiddleWare 函数接收若干个中间件作为参数
* 执行的返回结果是一个函数,对应 createStore 中的第三个参数 enhancer
* 用来接收旧的 createStore,返回一个新的 createStore,其内部的 dispatch 是经过增强的
*/
export function applyMiddleWare(...middleWares) {
return function enhancer(createStore) {
return function(reducer, initialState) {
const oldStore = createStore(reducer, initialState);
let dispatch = oldStore.dispatch;
// 传入 store,进行第一次调用
const middleWareChain = middleWares.map(middleWare => middleWare(oldStore));
middleWareChain.reverse().forEach(middle => {
dispatch = middle(dispatch)
})
return {
...oldStore,
dispatch,
}
}
}
}
使用 compose 聚合中间件,同时保证中间件执行顺序
❝compose 是函数式编程里常用的方法,作用是把类似于
❞var a = fn1(fn2(fn3(fn4(x))))这种嵌套的调用方式改成var a = compose(fn1,fn2,fn3,fn4)(x)。compose 的运行结果是一个函数,调用这个函数传递的参数将会成为 compose 最后一个参数成员的参数,从而实现类似洋葱圈的从内向外,逐步调用方式:
function compose(...funcs) {
if (funcs.length === 0) return () => {};
if (funcs.length === 1) return funcs[0];
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
...
dispatch = compose(...middleWareChain)(dispatch)
return {
...oldStore,
dispatch,
}
- 写一个简单的 thunk 中间件,测试异步 action:
// thunk.js
export default function thunk(store) {
return function (middleWare) {
return function(action) {
if (typeof action === 'function') return action(middleWare, store);
return middleWare(action)
}
}
}
总结
- 这样 redux 的核心功能就实现了,其实主要就是其中间件的实现。此外还有
combineReducers和用于注销订阅的unsubscribe函数,比较简单,可以直接去看源码了。
本文使用 mdnice 排版