redux 工作原理
Redux
和 React
之间并没有什么关系,脱离了 React
,Redux
也可以与其它的 js 库(甚至是原生 js)搭配使用,Redux
只是一个状态管理库,但它与 React
搭配时却很好用,使开发 React 应用更加简介。而使用 Redux 库时,需要先做“配置”,因为这些代码的书写是必不可少的。下面的图是 redux 的工作流:
redux 工作流
首先,react 组件从 store 中获取原始的数据,然后渲染。当 react 中的数据发生改变时,react 就需要使用 action,让 action 携带新的数据值派发给 store,store 把 action 发给 reducer 函数,reducer 函数处理新的数据然后返回给 store,最后 react 组件拿到更新后的数据渲染页面,达到页面更新的目的。
需要注意的是,在使用 Redux 时,最好不要监视最外层的容器,这样会把整个页面重新渲染,这是很浪费的,你应该绑定到像 App 这样的容器组件中。然后在容器组件中通过 props 向展示组件传递数据。
有关容器组件和展示组件的定义,可以参看这篇文档:
Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想[1]
实现 Redux
首先捋一下思路,Redux 库中都有哪些函数?这些函数的参数都有哪些?参数类型是什么?执行函数后会返回什么?下面就一一介绍一下 redux 中的函数,当然在实际的 redux 源码中要复杂一些,不过在这篇文章中核心概念是一样的。
1. createStore
该函数会创建一个 store,专门用于存储数据。他返回四个函数:
- dispatch:用于派发 action;
- getState:用于获得 store 中的数据;
- subscribe:订阅函数,当 state 数据改变后,就会触发监听函数;
- replaceReducer:reducer 增强器;
createStore
可以接收三个参数:
- reducer - 我们自己写的 reducer 函数;
- preloadedState - 可选参数,表示默认的 state 值,没有这个参数时,enhancer 可以是 createStore 的第二个参数;
- enhancer - 可选参数,增强器,比如 applyMiddleware 就是一个 enhancer;
该函数的模样:
代码语言:javascript复制function createStore(reducer,preloadedState,enhancer){
let state;
// 用于存放被 subscribe 订阅的函数(监听函数)
let listeners = [];
// getState 是一个很简单的函数
const getState = () => state;
return {
dispatch,
getState,
subscribe,
replaceReducer
}
}
那就来一一实现各个“子函数”。
dispatch
该函数是派发 action 的,因此会接受一个 action 对象作为参数:
代码语言:javascript复制function dispatch(action) {
// 通过 reducer 返回新的 state
// 这个 reducer 就是 createStore 函数的第一个参数
state = reducer(state, action);
// 每一次状态更新后,都需要调用 listeners 数组中的每一个监听函数
listeners.forEach(listener => listener());
return action; // 返回 action
}
subscribe
这个函数主要是往 listeners
数组中放入监听函数,参数就是一个监听函数。它还会返回一个 unsubscribe
函数,用于取消当前的订阅。
function subscribe(listener){
listeners.push(listener);
// 函数取消订阅函数
return () => {
listeners = listeners.filter(fn => fn !== listener);
}
}
replaceReducer
顾名思义,这个函数可以替换 reducer,它传入一个 reducer 用以替代当前执行的 reducer 函数。
代码语言:javascript复制function replaceReducer(reducer){
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer;
}
需要注意的是,在源码中完成负值后还会再派发一个类型为 @@redux/INIT
的 action。
2. combineReducers
该函数接收一个对象参数,对象的值是小的 reducer 函数。combineReducers 函数会返回总的 reducer 函数。combineReducers 函数样子:
代码语言:javascript复制function combineReducers(reducers){
// 返回总的 reducer 函数,
// 与小的 reducer 函数功能一样,返回更新后的 state
return (state = {},action) => {
// ...
}
}
调用 combineReducers 函数:
代码语言:javascript复制import { combineReducers, createStore } from "redux";
import reducer1 from "./reducer1";
import reducer2 from "./reducer2";
// rootReducer 是一个新的 reducer 函数
const rootReducer = combineReducers({
reducer1,
reducer2
});
var store = createStore(rootReducer);
具体实现:
代码语言:javascript复制function combineReducers(reducers){
return (state = {},action) => {
// 返回的是一个对象,reducer 就是返回的对象
return Object.keys(reducers).reduce(
(accum,currentKey) => {
accum[currentKey] = reducers[currentKey](state[currentKey],action);
return accum;
},{} // accum 初始值是空对象
);
}
}
这里使用了 ES6 数组当中的 reduce
函数。首先拿出来对象的键进行遍历,accum 的初始值是一个空对象,currentKey 表示当前遍历的键。state[currentKey]
可能是没有的,默认值我们可能并没有指定,但并不影响。原因是这样的,state 对象中没有 currentKey
属性时,返回 undefined
,这时如果小的 reducer 指定了默认值,或者 createStore 指定了默认值,就会使用默认值。就像下面的代码:
function fn(a = 123){
console.log(a);
}
fn(undefined); // 123,当参数是 undefined 时会使用默认值
fn(456); // 456
combineReducers 函数返回一个 reducer 函数,当调用这个 reducer 函数时就会返回如下形式的对象:
代码语言:javascript复制{
reducer1: { count: 1 },
reducer2: { bool: true },
reducer3: { ... },
// ....
}
在写 React 时,可以通过 connect
中的 mapStateToProps
函数获取到 state,如果使用了 combineReducers
,那么获取特定容器组建的 reducer 的 state 是这样获取的:
mapStateToProps(state){
return {
// reducer1 就是 combineReducers 对象参数中的一个键(每个键对应一个 reducer 函数)
count: state.reducer1.count
}
}
需要注意的是,如果你使用了 combineReducers
,并且想把 state 初始值指定在 createStore
中,那么就要把默认值写成这种形式,不然小的 reducer 中的 state 参数就无法获取到默认值。
const defaultState = {
// 对象的键应与 combineReducers 函数传入的对象参数中的键相同
reducer1: {},
reducer2: {},
// ...
}
比如下面两个 reducer 没有指定 state 默认值,而是在 createStore 中指定的,当然这里直接给 rootReducer 指定的默认值,原理都是一样的,因为在 createStore
函数的 dispatch
函数中会调用 rootReducer 函数,把 createStore 中接收的默认 state 传入 rootReducer 函数中。
function reducer1(state,action){
switch (action.type) {
case "ADD": return { count: action.payload 1 };
case 'MINUS': return { count: action.payload - 1 };
default: return state;
}
}
function reducer2(state,action){
switch(action.type){
case 'SWITCH': return { bool: !state };
default: return state;
}
}
const rootReducer = combineReducers({reducer1,reducer2});
var state = rootReducer({
// 设置默认值
reducer1: { count: 1 },
reducer2: true
},{type: 'ADD', payload: 3});
3. applyMiddleware
实现之前先说一下这个函数,在使用时是把它传递给 createStore
的:
import { createStore,applyMiddleware } from "redux";
import reduxThunk from "redux-thunk";
var store = createStore(reducer,applyMiddleware(reduxThunk));
前面已经说了,createStore 的 enhancer 是一个函数,因此 applyMiddleware 执行后应该返回一个函数。enhancer
函数被称为增强器。enhancer 函数接收 createStore
函数作为参数,并又返回一个函数,这个函数有两个参数:reducer
和 preloadedState
,就是 createStore 的前两个参数。即:
function applyMiddleware(...middlewares){
return function(createStore){
return function(reducer,preloadedState){
// ...
// 最后把增强后的 store 返回
return {
...store,
// 因为改进了 dispatch,因此要把原来的 dispatch 覆盖掉
dispatch
}
}
}
}
可以发现,applyMiddleware 函数是一个三级柯里化函数:
代码语言:javascript复制applyMiddleware(...middlewares)(createStore)(reducer,preloadedState);
因此我们需要改造 createStore
函数,当有 enhancer
函数时就要调用 enhancer 函数:
function createStore(reducer,preloadedState,enhancer){
// ...
if(typeof preloadedState === "function" && typeof enhancer === 'undefined'){
enhancer = preloadedState;
preloadedState = undefined;
}
if(typeof enhancer !== "undefined"){
if(typeof enhancer !== "function"){
throw new Error("Expected the enhancer to be a function.");
}
// 如果有 enhance 函数,就执行 enhancer 函数,返回增强后的那四个 store 中的函数
return enhancer(createStore)(reducer,preloadedState);
}
// ...
}
具体实现
代码语言:javascript复制function applyMiddleware(...middlewares){
return function(createStore){
return function(reducer,initialState){
var store = createStore(reducer,initialState);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return { ...store, dispatch };
}
}
}
首先调用 createStore,创建出 store,拿到 store 当中的 dispatch 方法。middlewareAPI
是传递给中间件函数的参数,每个中间件在书写时都应该有一个参数,里面有 getState 方法和 dispatch 包装函数。而 chain 数组里面就是中间件函数。这时 dispatch 函数就可能有多个,但实际的 dispatch 只有一个,因此需要使用 compose
函数将多个 dispatch 函数变成一个。要想变成一个也很简单,compose 函数返回一个 dispatch 函数,该函数内部是所有 dispatch 函数的执行。在 redux 源码中 compose 函数大致是这样的:
function compose(...funcs) {
if (funcs.length === 0) {
// 当没有 dispatch 增强函数时,就返回一个函数
return arg => arg;
}
if (funcs.length === 1) {
// 当只有一个 dispatch 函数时,就直接返回
return funcs[0];
}
return funcs.reduce((accum, currentFn) => {
return (...args) => {
return accum(currentFn(...args));
}
});
}
下面解释一下这个函数的实现逻辑。首先需要了解到在 applyMiddleware
函数中,调用 compose 函数是这样调用的:
dispatch = compose(...chain)(store.dispatch);
compose 接收一个数组,会返回一个函数,这个函数将原始的 dispatch 作为参数传入,并返回一个全新的 dispatch 函数。在说实现逻辑之前,我们需要先了解一下中间件的概念,如果使用过 express 或者 koa 框架的话对中间件应该不会陌生。每个中间件都会有一个 next
函数,它指代下一个中间件,当调用 next
函数时就相当于调用了下一个中间件。在 redux 中也是如此,并且中间件是有顺序的,chain 数组最左侧的中间件会先调用,然后在内部调用 next 方法,表示执行下一个中间件。因为我们是改进 dispatch 函数,毫无疑问 next 其实就是每个中间件改进后的 dispatch 函数。当我们看 redux 中间件源码时就会发现每个中间件都是下面这个样子:
function middleware({ getState, dispatch }){
return next => action => {
// ....
next(action);
}
}
中间件拦截到 dispatch,做一些操作后,把 action 传给 next,自动执行下一个中间件函数。
现在再来看一下 compose 函数,如果数组 chain 中有值,那么它们都应该长这个样子(调用了 middleware 后会返回一个带有 next 参数的函数):
代码语言:javascript复制function fn(next){
return action => {
// ...
// 一些操作后调用 next,执行下一个中间件
next(action);
}
}
在 compose 函数中又使用了 reduce
函数,这里再说一下 reduce
函数,上面使用该函数实现 combineReducers
函数时有个初始值,而这里没有,当 reduce 函数不指定初始值时,会将数组的第一项作为初始值,currentFn 的第一次调用就变成了数组的第二项。看到 reduce 函数是估计有些晕,这里解释一下,reduce 每次都返回一个函数(accum),在这个函数内部,一个函数的执行结果(返回 dispatch 函数)会作为另一个函数的参数传入(next 参数),假如 chain 数组有两个函数:[a,b]
,当调用 compose 函数时,b 的执行结果会是一个 dispatch 函数,把这个函数传给 a,此时 a 的 next 就是 b 函数的返回值,当在 a 中执行 next 方法时,就会调用 b 中返回的那个函数。
b 也是一个中间件,因此 b 中返回的 dispatch 函数内部也应调用 next
方法,让下一个中间件去执行别的操作,但是如果 b 后面没有中间件了呢?没有中间件时就执行原始的 dispatch 函数,即:将 next 可以指向原始的 dispatch 函数,于是在 applyMiddleware 函数中就有了这种写法:
dispatch = compose(...chain)(store.dispatch);
调用 compose 函数时,返回的是一个大的中间件函数,store.dispatch
函数是中间件的 next
,因此调用中间件函数后会返回一个 dispatch。这个 dispatch 是数组最左侧的那个函数返回的 dispatch。当派发 action 时,就会执行 dispatch,dispatch 中的 next 函数也自然就会执行。
这也就是为什么 redux-logger
中间件为什么放在数组最右边,最左边的中间件会先执行,不那样做可能就无法打印出准确的 action 信息。
compose 函数代码虽然很少,但是里面使用了很多函数式编程的概念,比如柯里化函数、高阶函数等,让人看起来比较费解。
4. 写一个中间件
通过上面 applyMiddleware
函数内部可以看出,中间件的参数是接收一个对象,该对象中有两个函数:getState
和 dispatch
。我们使用这两个函数就可以做一些事情。以 redux-logger
中间件为例,该函数可以在 dispatch 派发时打印日志。它的结构大致是这样的:
function logger(middlewareAPI){
const { getState, dispatch } = middlewareAPI;
return next => {
// 返回一个全新的 dispatch 函数
return (action) => {
console.log(action.type);
console.log('action',action);
console.log('previous state', store.getState());
// 调用原始的 dispatch 函数并记录日志
const returnAction = next(action);
console.log('next state', getState());
console.log(action.type);
return returnAction;
}
}
}
而在 redux-logger 库中,使用 createLogger
函数时可以传递参数这是怎么做到的呢?其实也很简单,在上面 redux applyMiddleware 函数是一个柯里化函数,createLogger 也是如此:
function createLogger(options = {}){
// ... createLogger 的一些实现
return ({dispatch, getState}) => next => action => {
// ... 改进 dispatch 函数
}
}
// 用于挂载中间件的函数
function logger({ getState, dispatch } = {}){
if (typeof dispatch === 'function' || typeof getState === 'function') {
return createLogger()({ dispatch, getState });
}
}
当我们想要自定义配置时需要调用 createLogger
并传入配置参数。这时就会返回一个带有 dispatch
和 getState
的对象参数的函数,而这个函数与 logger
函数形式相同,于是直接使用这个函数作为中间件即可。也就是说,在不做配置时,我们可以直接使用 logger 函数,在配置参数时,我们需要使用 createLogger 的返回值作为中间件函数:
import { createLogger } from "redux-logger";
const logger = createLogger({
// options...
});
const store = createStore(reducer,applyMiddleware(logger));
redux-thunk
redux-thunk 实现起来就更简单了,先回顾一下 redux-thunk 的使用方式,要想用 dispatch 派发异步请求来的数据需要在定义一个函数,该函数返回一个函数,参数是 dispatch:
代码语言:javascript复制// actions.js
const ajaxDataAction = (data) => ({ type: "ajax_data", payload: data });
export function ajaxAction(){
// 这个 action 会返回一个函数
return (dispatch) => {
fetch("/info").then(json => json())
.then(data => {
// 得到数据后派发 action
dispatch(ajaxDataAction(data));
});
}
}
因此,redux-thunk 函数内部需要先拦截 dispatch 函数,判断 action 参数的数据类型是不是函数,如果是函数就执行函数:
代码语言:javascript复制function thunk({ getState, dispatch }){
return next => action => {
if(typeof action === "function"){
return action(dispatch, getState);
}
// 传递给下一个中间件
return next(action);
}
}
redux-thunk 源码大概也就那么多,但是 GitHub 上却有将近 15K 的 star。除了使用 redux-thunk 作为异步处理中间件之外,还可以使用 redux-saga,只是后者的学习成本会高一些。
通过分析可以了解,redux 库代码量虽然很少,只有六七百行,但是 redux 可以说是函数式编程的典范,对于一些代码的逻辑并不太好理解。
参考资料
[1]
Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想: https://www.redux.org.cn/docs/basics/UsageWithReact.html