redux详解

23,139 阅读14分钟

redux是一个独立的状态管理库,人们常会把它和react挂钩。但实际上redux是一个拥有完整独立闭环的工具库,它可以和任何的框架结合。当然redux和UI框架的结合实际也只是做了对应的封装,本质上redux还是一种状态管理的解决方案。

流程

整体流程非常简短精悍

模块分析

1、store

store是redux官方提供的createStore方法调用之后返回的一个js对象,它的ts类型如下

export interface Action<T = any> {
  type: T
}
interface AnyAction extends Action {
  // Allows any extra properties to be defined in an action.
  [extraProps: string]: any
}
type Unsubscribe = () => void; //取消订阅
interface Dispatch<A extends Action = AnyAction> {
  <T extends A>(action: T): T
}
export type Reducer<S = any, A extends Action = AnyAction> = (
  state: S | undefined,
  action: A
) => S    
interface Store {
    dispatch: Dispatch; //修改redux state的唯一方法
    subscribe: (listener) => Unsubscribe; //订阅listener,用于和外部框架(例如react)的结合
    getState: () => State; //获取redux中的state
    replaceReducer: (nextReducer) => void; //替换当前的reducer(常用于热更新)
}

下述是redux官方提供的createStore方法,摘取了一些核心代码,去掉了一些边界值判定

export default function createStore(reducer, preloadedState, enhancer) {
	/**
	 * redux提供创建Store的方法
	 * @param {(state: State, action: Action) => State} reducer // 纯函数,在dispatch方法中调用,根据dispatch方法参数中的action.type 处理后返回新的state
	 * @param {State} preloadedState //初始化的state
	 * @param {Function} enhancer //对于createStore方法的增强(middleware)
	 * @returns {Store} //返回一个js对象
	 */

	// 下面的源码,为了更加清新的阅读,删除了多数边界值的判定以及像isDispatching标识的使用场景
	// 实际返回的store中还有个属性[?observable]: observable (主要用于observable/reactive),本文不做展开,已经在当前方法返回值中删除了

	if (typeof enhancer !== 'undefined') {
		if (typeof enhancer !== 'function') {
			throw new Error('Expected the enhancer to be a function.');
		}
		// enhancer方法是为了对createStore方法的增强,主要目的是注入middleware
		return enhancer(createStore)(reducer, preloadedState);
	}

	let currentReducer = reducer; //reducer
	let currentState = preloadedState; // getState方法实际返回值
	let currentListeners = []; //listeners subscribe方法添加的listener会push到此数组,功能用于dispatch方法调用后,reducer返回新的state后 调用这些listeners
	let nextListeners = currentListeners; //主要是为了subscribe添加listener中时区分currentListeners
	let isDispatching = false; //是否正在dispatch的标识

	function ensureCanMutateNextListeners() {
		if (nextListeners === currentListeners) {
			// 相当于做了一层浅拷贝
			nextListeners = currentListeners.slice();
		}
	}

	/**
	 * getState方法就是返回当前的state
	 * currentState采用闭包的方式实现私有属性,外部无法直接访问currentState,只能通过getState方法调用返回
	 * @returns {State}
	 */
	function getState() {
		return currentState;
	}

	/**
	 * 添加一个state change 的listener. 在action被dispatch后触发执行该listener
	 * subscribe方法是redux和外部结合的渠道,通过发布订阅模式实现redux 中state更新时对外部通知并作出对应响应的方式
	 * @param {Function} listener 相当于dispatch方法调用后的回调函数
	 * @returns {Function} 取消当前订阅的listener的方法
	 */
	function subscribe(listener) {
		ensureCanMutateNextListeners();
		nextListeners.push(listener); //添加新的listener
		return function unsubscribe() {
			ensureCanMutateNextListeners();
			const index = nextListeners.indexOf(listener);
			nextListeners.splice(index, 1); //删除当前listener
		};
	}

	/**
	 * Dispatches an action. It is the only way to trigger a state change.
	 * 官方解释如上,dispatch是唯一触发state更新的方式
	 * @param {Action} action 此参数用于reducer生成新的state
	 * @returns {Action} 为了方便, 返回当前的action
	 */
	function dispatch(action) {
		try {
			currentState = currentReducer(currentState, action);
		} finally {
			// isDispatching标识的用处不做详细解释,文中对于多数边界值做了删除
			isDispatching = false;
		}
		const listeners = (currentListeners = nextListeners);
		for (let i = 0; i < listeners.length; i++) {
			const listener = listeners[i];
			listener();
		}
		return action;
	}

	/**
	 * 替换当前reducer,并调用一次dispatch方法
	 * 常用于热更新场景
	 * @param {Function} nextReducer The reducer for the store to use instead.
	 * @returns {void}
	 */
	function replaceReducer(nextReducer) {
		currentReducer = nextReducer;
		dispatch({
			type: ActionTypes.REPLACE
		});
	}

	// 创建store的同时会触发一次初始化,如下
	dispatch({
		type: ActionTypes.INIT
	});
	return {
		dispatch,
		subscribe,
		getState,
		replaceReducer
	};
}

上述源码已经解读的比较清晰,阅读时候建议折叠具体的方法,先看整体流程再看细节。

2、dispatch

下面是官方提供的dispatch方法源码的核心部分

/**
 * Dispatches an action. It is the only way to trigger a state change.
 * 官方解释如上,dispatch是唯一触发state更新的方式
 * @param {Action} action 此参数用于reducer生成新的state
 * @returns {Action} 为了方便, 返回当前的action
 */
function dispatch(action) {
	try {
		currentState = currentReducer(currentState, action);
	} finally {
		// isDispatching标识的用处不做详细解释,文中对于多数边界值做了删除
		isDispatching = false;
	}
	const listeners = (currentListeners = nextListeners);
	for (let i = 0; i < listeners.length; i++) {
		const listener = listeners[i];
		listener();
	}
	return action;
}

redux官方提供的applyMiddleware方法,增加了dispatch中间层,实现方式非常有意思,将在后续文章详细解读。

3、reducer

一开始在还没去阅读redux源码的时候,看了一些文章,我一直在想,reducer为什么叫做reducer,难道不是更应该叫做stateChange,后来才发现一开始的理解就是错误的。reducer并不是state发生改变后的回调,而是dispatch方法内部的调用,经过reducer调用之后生成新的state,赋值给redux内部的currentState。翻阅了官方文档,实际上很简单,就是参考的数组的reduce方法,它的核心也就是在于把前一次函数的返回值作为下一次函数调用的参数,一种类似递归的方式。

并且reducer应该是个纯函数,它的调用和返回值应该是可预测的。所以它并不适合增加一些side effect(副作用)例如log甚至添加localStorage之类。例如很多人喜欢使用reducer去写localStorage,这是一种非常错误的写法,这可能会让原本是纯函数的reducer不可预测,正确的做法应该是在subscribe添加listeners里面去写,或者使用中间件在具体的dispatch时候去添加。

常见reducer

const INITIAL_STATE: State = {
	count: 0
};
const reducers = (state: State = INITIAL_STATE, action: ActionType<typeof actions>) => {
	switch (action.type) {
		case actions.SET_COUNT:
			return {
				...state,
				count: ++state.count
			};
		default:
			return state;
	}
};

Array.prototype.reduce方法具有非常巧妙的应用场景,例如reduxapplyMiddleware中的compose方法,具体分析会在后面的文章体现

高级用法

Middleware

所谓中间件,个人理解就是输入输出的中间做一些特殊的处理,例如最常见的增加log。

针对reduxmiddleware,按照官方的说法: It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer 翻译过来就是说 提供了一个第三方扩展点: 在dispatch一个action和达到reducer的时刻,结合上文的dispatch源码,实际上redux-middleware的核心功能就是在dispatch方法的调用前后增加一些额外操作。下文将会以增加log作为基础的额外操作结合redux官方middleware案例进行说明。

在下面的案例之前,我们再把dispatch方法的内容整理一遍:

dispatch方法接受一个action参数,action是一个简单对象。然后会调用reducer方法(把当前action传入),reducer方法会根据action.type返回一个新的state,最后遍历&调用listeners

const action = {type: 'ADD_TODO'};
dispatch(action);

切入点-Logging

redux状态管理的一个优点就是在于状态变化的可预测和透明性(得益于state更新途径的唯一性&reducer的纯函数特质)例如我们有一个需求,需要把每一次dispatch方法的调用以及nextState(reducer返回的新state)打印出来。

例如下图,如有异常情况,我们可以很清晰的知道是哪一次dispatch的调用出了问题。

尝试1-简单暴力

没错,我们可以在所有的dispatch方法调用前后添加console.log!但是,请停下这样的骚操作,因为code-review的时候你可能会被你的领导一顿毒打!

const action = {type: 'ADD_TODO'}
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())

尝试2-包装dispatch

是的,上面简单暴力的全局添加各种日志可能会让你挨一顿毒打,那么我把骚操作封装成一个方法不就完事了~

function dispatchAndLog(store, action) {
  console.log('dispatching', action)
  store.dispatch(action)
  console.log('next state', store.getState())
}
const action = {type: 'ADD_TODO'}
dispatchAndLog(store, action)

等一等,小伙子,为毛你的项目满屏的dispatchAndLog (import&invoke),这都是些啥玩意!

尝试3-Monkeypatching Dispatch(猴子补丁)

有必要先科普一下所谓的Monkeypatching,实际上它在我们的项目中无处不在,尤其是第三方工具库中。

简单的来说就是修改原有的对象属性,比如有一个第三方库lib,它的getValue属性不符合我的需求,我要把它给改了。

var lib = require("lib");
lib.getValue = () => '就是这么任性'

针对于store.dispatch猴子补丁

const next = store.dispatch // 缓存store.dispatch的初始方法
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

看上去很完美,再也不用到处导入&调用上面的dispatchAndLog方法,dispatch的用法也是和原有的一样。

但是这个时候除了在dispatch方法调用前后增加log,还需要增加一个额外操作,为了保证项目的稳定运行,避免各种骚操作抛出的异常,需要对dispatch方法进行try-catch处理

下面进行如下代码改造:

function patchStoreToAddLogging(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreToAddCrashReporting(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      })
      throw err
    }
  }
}
// 调用
patchStoreToAddLogging(store);
patchStoreToAddCrashReporting(store); //这个时候store.dispatch方法实际上已经是dispatchAndLog方法

尝试4-Hiding Monkeypatching

上面的案例里面,我们还是明目张胆的在每个middleware中修改了store.dispatch方法,很任性,但是实际场景的middleware应该是对dispatch方法没有side effect,各个中间件只需要考虑本身需要实现的部分,并不应该去主动的修改dispatch方法,这个时候我们需要做一些封装,让middleware更多的关注本身需要实现的功能,而不去关注在中间件中修改dispatch方法。

首先,我们将中间件做一个改造,改为返回一个函数。

function logger(store) {
  const next = store.dispatch

  // Previously:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

然后我们可以再做一层封装:

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice(); // 浅拷贝
  middlewares.reverse(); // 元素反转 

  // 转换dispatch方法
  middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}

这个时候,我们的调用如下:

applyMiddlewareByMonkeypatching(store, [logger, crashReporter]);

非常好,成功的在middleware中隐藏了Monkeypatching

但是,依然还是Monkeypatching,只不过封装在了applyMiddlewareByMonkeypatching方法中而已。

尝试5-Removing Monkeypatching

上面的Monkeypatching案例的目的是什么,修改dispatch方法的目的是啥?

整理一下,当中间件只有一个的时候(例如增加日志),目的就很清晰,就是简单的对dispatch方法的增强,也就是在dispatch方法调用前后增加额外操作

当中间有多个的时候,目的就是让后一个中间件访问的dispatch方法实际是前一个中间件调用包装之后的新的dispatch方法。

注意,核心点---后一个中间件访问的dispatch方法实际是前一个中间件调用包装之后的新的dispatch方法

wait~这个核心点的非常靠近了函数式编程中常用的compose方法(后文会详细介绍)

那么,我们可以考虑再装一层封装,dispatch方法不再直接从store.dispatch中获取,而是从函数参数中获取。

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}

再用es6的箭头函数再造下:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}
// 简单的调用
const action = {type: 'ADD_TODO'};
// withLoggerWrapper是logger执行后返回的一个类似dispatch方法。
const dispatchWithLoggerWrapper = logger(store)(store.dispatch);
const dispatchWithCrashReporterWrapper = crashReporter(store)(dispatchWithLoggerWrapper);
// 最终的调用
dispatchWithCrashReporterWrapper(action);

上面的代码,第一层中间件logger调用后返回一个方法,将作为第二个中间件的入参。以此类推,这样,store.dispatch方法就没有被修改,仅仅只是作为中间件的参数传入&调用而已。

至此,核心的redux-middleware方案已出,剩下的就是中间件的应用。

applyMiddleware相关

上文已经整理了redux-middleware核心思路,接着就是对于中间件的应用。结合前文的源码分析,我们可以容易的得出,中间件的应用是在createStore方法调用的时候

下面的redux库中的applyMiddleware方法源码:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

思路非常清晰,就是传入中间件,返回一个新的createStore方法,本质上也就是对于createStore方法的增强,使其最终返回的对象里面的dispatch方法是经过中间件包装后的新的方法,并不会对原有store.dispatch做修改。

我们来分析上文的代码

const middlewareAPI = {
	getState: store.getState,
	dispatch: (...args) => dispatch(...args)
};
// 下面chain就是给所有的middleware调用store(核心api)之后返回的数组。
const chain = middlewares.map(middleware => middleware(middlewareAPI));

首先是核心的middlewares初步包装(给每个middleware传入store

根据上文的最终确定的redux-middleware个格式(如下)

const middleware = store => next => action => {
  // 一些操作
  // 例如打印action
  console.log(action);
  let result = next(action)
  // 一些操作
  // 例如打印nextState
  console.log(store.getState());
  return result;
}

可以推算出最终的chain就是个下面这个形式的数组,核心目的就是给每个middleware绑定store

// 最终的chain就是下面的数组,形式,
chain = [
			next => action => {
				console.log('中间件1');
				return next(action);
			},
			next => action => {
				console.log('中间件2');
				return next(action);
			}
];

然后终于到了前文多次提及的compose大法:

/**
 * 从右到左整合单参数函数
 * 最右侧的函数参数可以为多参数
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} 整合后的参数
 * 举个例子, compose(f, g, h) 的返回值等同于(...args) => f(g(h(...args)))
 */
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

不得不称赞强大的reduce,来做个简单的测试:

const fn1 = () => console.log('fn1');
const fn2 = () => console.log('fn2');
const fn3 = (...args) => console.log(args);
compose(fn1, fn2, fn3)(1, 2, 3);
// reduce方法中callback第一次调用:
// 因为没有初始值,上面的a就是fn1, b就是fn2
// 那么第一次的调用返回值就是(...args) => fn1(fn2(...args))
// reduce方法中callback第二次调用:
// 这时的a是上一次callback执行后的返回值也就是(...args) => fn1(fn2(...args)), b就是fn3
// 那么第二次的调用返回值就是(...args) => fn1(fn2((fn3(...args))))
// 最终的日志就是
[1, 2, 3]
fn2
fn1

接下啦我们再来复盘一下整个调用经过。

		const store = createStore(...args); // 初始化一个store, 入参不需要关注
		let dispatch = () => { // 定义一个dispatch
			throw new Error(
				'Dispatching while constructing your middleware is not allowed. ' +
					'Other middleware would not be applied to this dispatch.'
			);
		};
		const middleware1 = store => next => action => {
			console.log('中间件1');
			return next(action);
		};
		const middleware2 = store => next => action => {
			console.log('中间件2');
			return next(action);
		};
		const middlewareAPI = {
			getState: store.getState,
			dispatch: (...args) => dispatch(...args)
		};
		// 下面chain就是给所有的middleware调用store(核心api)之后返回的数组。
		const chain = middlewares.map(middleware => middleware(middlewareAPI));
		dispatch = compose(...chain)(store.dispatch);

chain的值

chain = [
			next => action => {
				console.log('中间件1');
				return next(action);
			},
			next => action => {
				console.log('中间件2');
				return next(action);
			}
];
// 我们再来简单处理一下, 将chain当做下面的内容
chain = [
  	mid1,
  	mid2
];

所以compose(...chain)之后的结果是

(...args) => mid1(mid2(...args));

所以compose(...chain)(store.dispatch)的结果是:

mid1(mid2(store.dispatch));

我们再把mid1,mid2还原,那就是

// mid2(store.dispatch)调用后的结果 相当于下面的函数
action => {
  console.log('中间件2');
	return store.dispatch(action);
};

我们将上面mid2(store.dispatch)返回的函数赋值给变量next,然后继续:

mid1(mid2(store.dispatch)) === mid1(next);
mid1(next); // 运行结果就是显而易见了
// 运行返回下面函数,就是最终的中间件
action => {
	console.log('中间件1');
  console.log('中间件2');
	return store.dispatch(action);
};

整理一下applyMiddleware方法:

1、接受...middlewares入参

2、最终返回一个新的createStore方法

3、新createStore方法返回一个新的store,和原有的store差别就是在dispatch方法是经过middlewares包装后的方法。

总结

总体redux源码非常简短精悍,是一个完整的数据管理工具库,对外提供的api(store.subscribe)也非常简单,所以它实际可以和各种框架类库结合,例如和react的结合就使用了react-redux的实现,实现的原理也非常简单易懂。后续文章会继续会补充react-redux实现原理等。