从 Redux 源码谈谈函数式编程

49 阅读18分钟

目录

  1. 基本概念;
  2. 基本使用;
  3. Redux Core 源码分析(包括 createStoreapplyMiddleware);
  4. 处理异步 Action(为什么用 Redux thunk 和实现原理);

在 React 的世界中,状态管理方案不下几百种,但其中最经典的,莫过于 Redux 。如果你想学习函数式编程,那么 Redux 源码就是最好的学习材料。考虑到不少小伙伴用的是 Vue ,本人争取让这篇文章写得通俗易懂,让没有接触过 React 技术栈的同学也能掌握 Redux 。

Redux 属于典型的“百行代码,千行文档”,其中核心代码非常少,但是思想不简单,可以总结为下面两点:

  • 全局状态唯一且不可变(Immutable) ,不可变的意思是当需要修改状态的时候,用一个新的来替换,而不是直接在原数据上做更改:

    let store = { foo: 1, bar: 2 };
    
    // 当需要更新某个状态的时候
    // 创建一个新的对象,然后把原来的替换掉
    store = { ...store, foo: 111 };
    

    这点与 Vue 恰好相反,在 Vue 中必须直接在原对象上修改,才能被响应式机制监听到,从而触发 setter 通知依赖更新

  • 状态更新通过一个纯函数(Reducer)完成。纯函数(Pure function)的特点是:

    • 输出仅与输入有关;
    • 引用透明,不依赖外部变量;
    • 不产生副作用;

    因此对于一个纯函数,相同的输入一定会产生相同的输出,非常稳定。使用纯函数进行全局状态的修改,使得全局状态可以被预测。

1. 需要了解的几个概念

在使用 Redux 及阅读源码之前需要了解下面几个概念:

Action

action 是一个普通 JavaScript 对象,用来描述如何修改状态,其中需要包含 type 属性。一个典型的 action 如下所示:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

Reducers

reducer 是一个纯函数,其函数签名如下:

/**
 * @param {State} state 当前状态
 * @param {Action} action 描述如何更新状态
 * @returns 更新后的状态
 */
function reducer(state: State, action: Action): State

reducer 函数的名字来源于数组的 reduce 方法,因为它们类似数组 reduce 方法传递的回调函数,也就是上一个返回的值会作为下一次调用的参数传入。

reducer 函数的编写需要严格遵顼以下规则:

  • 检查 reducer 是否关心当前的 action
    • 如果是,就创建一份状态的副本,使用新的值更新副本中的状态,然后返回这个副本
  • 否则就返回当前状态

一个典型的 reducer 函数如下:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  if (action.type === 'counter/incremented') {
    return {
      ...state,
      value: state.value + 1
    }
  }
  return state
}

Store

通过调用 createStore 创建的 Redux 应用实例,可以通过 getState() 方法获取到当前状态。

Dispatch

store 实例暴露的方法。更新状态的唯一方法就是通过 dispatch 提交 actionstore 将会调用 reducer 执行状态更新,然后可以通过 getState() 方法获取更新后的状态:

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

storeEnhancer

createStore 的高阶函数封装,用于增强 store 的能力。Redux 的 applyMiddleware 是官方提供的一个 enhancer 。

middleware

dispatch 的高阶函数封装,由 applyMiddleware 把原 dispatch 替换为包含 middleware 链式调用的实现。Redux-thunk 是官方提供的 middleware ,用于支持异步 action

2. 基本使用

学习源码之前,我们先来看下 Redux 的基本使用,便于更好地理解源码。

首先我们编写一个 Reducer 函数如下:

// reducer.js
const initState = {
  userInfo: null,
  isLoading: false
};

export default function reducer(state = initState, action) {
  switch (action.type) {
    case 'FETCH_USER_SUCCEEDED':
      return {
        ...state,
        userInfo: action.payload,
        isLoading: false
      };
    case 'FETCH_USER_INFO':
      return { ...state, isLoading: true };
    default:
      return state;
  }
}

在上面代码中:

  • reducer 首次调用的时候会传入 initState 作为初始状态,然后 switch...case 最后的 default 用来获取初始状态
  • switch...case 中还定义了两个 action.type 用来指定如何更新状态

接下来我们创建 store :

// index.js
import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);

store 实例会暴露两个方法 getStatedispatch ,其中 getState 用于获取状态,dispatch 用于提交 action 修改状态,同时还有一个 subscribe 用于订阅 store 的变化:

// index.js

// 每次更新状态后订阅 store 变化
store.subscribe(() => console.log(store.getState()));

// 获取初始状态
store.getState();

// 提交 action 更新状态
store.dispatch({ type: "FETCH_USER_INFO" });
store.dispatch({ type: "FETCH_USER_SUCCEEDED", payload: "测试内容" });

我们运行一下上面的代码,控制台会先后打印:

{ userInfo: null, isLoading: false } // 初始状态
{ userInfo: null, isLoading: true } // 第一次更新
{ userInfo: "测试内容", isLoading: false } // 第二次更新

3. Redux Core 源码分析

上面的例子虽然很简单,但是已经包含 Redux 的核心功能了。接下来我们来看下源码是如何实现的。

createStore

可以说 Redux 设计的所有核心思想都在 createStore 里面了。createStore 的实现其实非常简单,整体就是一个闭包环境,里面缓存了 currentReducercurrentState,并且定义了 getStatesubscribedispatch 等方法。

createStore 的核心源码如下,由于这边还没用到 storeEnhancer ,开头有些 if...else 的逻辑被省略了,顺便把源码中的类型注解也都去掉了,方便阅读:

// src/createStore.ts
function createStore(reducer, preloadState = undefined) {
  let currentReducer = reducer;
  let currentState = preloadState;
  let listeners = [];

  const getState = () => {
    return currentState;
  }

  const subscribe = (listener) => {
    listeners.push(listener);
  }

  const dispatch = (action) => {
    currentState = currentReducer(currentState, action);

    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }

    return action;
  }
  
  dispatch({ type: "INIT" });

  return {
    getState,
    subscribe,
    dispatch
  }
}

createStore 的调用链路如下:

  • 首先调用 createStore 方法,传入 reducerpreloadStatepreloadState 代表初始状态,假如不传那么 reducer 必须要指定初始值;
  • reducerpreloadState 分别赋值给 currentReducercurrentState 用于创建闭包;
  • 创建 listeners 数组,这其实就是基于发布订阅模式,listeners 就是发布订阅模式的事件中心,也是通过闭包缓存;
  • 创建 getStatesubscribedispatch 等函数;
  • 调用 dispatch 函数,提交一个 INIT 的 action 用来生成初始 state ,在 Redux 源码中,这里的 type 是一个随机数;
  • 最后返回一个包含 getStatesubscribedispatch 函数的对象,即 store 实例;

那么很显然,外界无法访问到闭包的值,只能通过 getState 函数访问。

为了订阅状态更新,可以使用 subscribe 函数向事件中心 push 监听函数(注意 listener 是允许副作用存在的)。

当需要更新状态时,调用 dispatch 提交 action 。在 dispatch 函数中调用 currentReducer(也就是 reducer 函数),并传入 currentStateaction ,然后生成一个新的状态,传给 currentState 。在状态更新完成后,将订阅的监听函数执行一遍(实际上只要调用 dispatch ,即使没有对 state 做任何修改,也会触发监听函数)。

如果有熟悉面向对象编程的小伙伴可能会说,createStore 里面做的事情可以封装到一个类里面。确实可以,本人用 TypeScript 实现如下(发布订阅的功能不写了):

type State = Object;
type Action = {
  type: string;
  payload?: Object;
}
type Reducer = (state: State, action: Action) => State;

// 定义 IRedux 接口
interface IRedux {
  getState(): State;
  dispatch(action: Action): Action;
}

// 实现 IRedux 接口
class Redux implements IRedux {
  // 成员变量设为私有
  // 相当于闭包作用
  private currentReducer: Reducer;
  private currentState?: State;

  constructor(reducer: Reducer, preloadState?: State) {
    this.currentReducer = reducer;
    this.currentState = preloadState;
    this.dispatch({ type: "INIT" });
  }
  
  public getState(): State {
    return this.currentState;
  }

  public dispatch(action: Action): Action {
    this.currentState = this.currentReducer(
      this.currentState,
      action
    );
    return action;
  }
}

// 通过工厂模式创建实例
function createStore(reducer: Reducer, preloadState?: State) {
  return new Redux(reducer, preloadState);
}

你看,多有意思,函数式编程和面向对象编程竟然殊途同归了

applyMiddleware

applyMiddleware 是 Redux 中的一个难点,虽然代码不多,但是里面用到了大量函数式编程技巧,本人也是经过大量源码调试才彻底搞懂。

首先要能看懂这种写法:

const middleware =
  (store) =>
    (next) =>
      (action) => {
        // ...
      }

上面的写法相当于:

const middleware = function(store) {
  return function(next) {
    return function(action) {
      // ...
    }
  }
}

其次需要知道,这种其实就是函数柯里化,也就是可以分步接受参数。如果内层函数存在变量引用,那么每次调用都会生成闭包。

说到闭包,有些同学马上就想到内存泄漏。但实际上闭包在平时项目开发中非常常见,很多时候我们不经意间就创建了闭包,但往往都被我们忽略了。

闭包一大作用就是缓存值,这和声明一个变量在赋值的效果是类似的。而闭包的难点就在于,变量是显式声明,而闭包往往是隐式的,什么时候创建闭包,什么时候更新了闭包的值,很容易被忽略。

可以这么说,函数式编程就是围绕闭包展开的。在下面的源码分析中,会看到大量闭包的例子。

applyMiddleware 是 Redux 官方实现的 storeEnhancer ,实现了一套插件机制,增加 store 的能力,例如实现异步 Action ,实现 logger 日志打印,实现状态持久化等等。

export default function applyMiddleware<Ext, S = any>(
  ...middlewares: Middleware<any, S, any>[]
): StoreEnhancer<{ dispatch: Ext }>

个人观点,这样做的好处就是提供了造轮子的空间

applyMiddleware 接受一个或多个 middleware 实例,然后再传给 createStore

import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk"; // 使用 thunk 中间件
import reducer from "./reducer";

const store = createStore(reducer, applyMiddleware(thunk));

createStore 入参中只接受一个 storeEnhancer ,如果需要传入多个,可以使用 Redux Utils 中的 compose 函数将它们组合起来。

compose 函数在后面会介绍

看上面的用法,可以猜测 applyMiddleware 肯定也是个高阶函数。之前说到 createStore 前面有些 if..else 逻辑因为没用到 storeEnhancer 所以被省略了。这边我们一起来看下。

首先看 createStore 的函数签名,实际上是可以接受 1-3 个参数。其中 reducer 是必须要传递的。当第二个参数为函数类型,会识别为 storeEnhancer。如果第二个参数不是函数类型,则会识别为 preloadedState ,此时还可以再传递一个函数类型的 storeEnhancer :

function createStore(reducer: Reducer, preloadedState?: PreloadedState | StoreEnhancer, enhancer?: StoreEnhancer): Store

可以看到源码中参数校验的逻辑:

// src/createStore.ts:71
if (
  (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
  (typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
  // 传递两个函数类型参数的时候,抛出异常
  // 也就是只接受一个 storeEnhancer
  throw new Error();
}

当第二个参数为函数类型,将它作为 storeEhancer 处理:

// src/createStore.ts:82
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
  enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
  preloadedState = undefined
}

接下来是一个比较难的逻辑:

// src/createStore.ts:87
if (typeof enhancer !== 'undefined') {
  // 如果使用了 enhancer
  if (typeof enhancer !== 'function') {
    // 如果 enhancer 不是函数就抛出异常
    throw new Error();
  }

  // 直接返回调用 enhancer 之后的结果,并没有往下继续创建 store
  // enhancer 肯定是一个高阶函数
  // 先传入了 createStore,又传入 reducer 和 preloadedState
  // 说明很有可能在 enhancer 内部再次调用 createStore
  return enhancer(createStore)(
    reducer,
    preloadedState
  )
}

下面我们来看一下 applyMiddleware 的源码,为便于阅读,把源码中的类型注解都去掉了:

// src/applyMiddleware.ts
import compose from './compose';

function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState);
    let dispatch = () => {
      throw new Error();
    }

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

    return {
      ...store,
      dispatch
    }
  }
}

可以看到这里代码并不多,但是出现了一个函数嵌套函数的情形:

const applyMiddleware = (...middlewares) =>
  (createStore) =>
    (reducer, preloadedState) => {
      // ...
    }

分析一下源码中的调用链路:

  • 调用 applyMiddleware 时,传入中间件实例,返回 enhancer 。从剩余参数的用法看出,支持传入多个 middleware
  • createStore 调用 enhancer ,分两次传入 createStorereducerpreloadedState
  • 内部再次调用 createStore ,这次由于没有传 enhancer ,所以直接走创建 store 的流程;
  • 创建一个经过修饰的 dispatch 方法,覆盖默认 dispatch
  • 构造 middlewareAPI ,对 middleware 注入 middlewareAPI
  • 将 middleware 实例组合为一个函数,再向 middleware 传递默认的 store.dispatch 方法;
  • 最后返回一个新的 store 实例,此时 storedispatch 方法经过了 middleware 修饰;

这里涉及到 compose 函数,是函数式编程范式中经常用到的一种处理,创建一个从右到左的数据流,右边函数执行的结果作为参数传入左边,最终返回一个以上述数据流执行的函数:

// src/compose.ts:46
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))
  )
}

思考题:如果希望把执行顺序改为从左往右,需要怎么改?

通过这边的代码,我们不难推断出一个中间件的结构:

function middleware({ dispatch, getState }) {
  // 接收 middlewareAPI
  return function(next) {
    // 接收默认的 store.dispatch 方法
    return function(action) {
      // 接收组件调用 dispatch 传入的 action
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      return result;
    }
  }
}

看到这里,我想大多数读者都会有两个问题:

  1. 通过 middlewareAPI 获取的 dispatch 函数和 store 实例最终暴露的 dispatch 函数都是经过修饰的吗;
  2. 为了防止在创建 middleware 的时候调用 dispatchapplyMiddleware 给新的 dispatch 初始化为一个空函数,且调用会抛出异常,那么这个函数究竟在何时被替换掉的;

大家可以先试着思考一下。

说实话,本人在阅读源码的时候也被这两个问题困扰,大多数技术文章也都没有给出解释。没办法,只能通过调试源码来找答案。经过不断调试,终于搞清楚了,middlewareAPIdispatch 函数本身其实就是以闭包形式引入的,这个闭包可能没多少人能看得出来:

// 定义新的 dispatch 方法
// 此时是一个空函数,调用会抛出异常
let dispatch = () => {
  throw new Error();
}
// 定义 middlewareAPI
// 注意这里的 dispatch 是通过闭包形式引入的
const middlewareAPI = {
  getState: store.getState,
  dispatch: (action, ...args) => dispatch(action, ...args)
}
// 对 middleware 注入 middlewareAPI
// 此时在 middleware 中调用 dispatch 会抛出异常
const chain = middlewares.map(middleware => middleware(middlewareAPI));

然后下面这段代码其实做了两件事,一方面将 middleware 组合为一个函数,注入默认 dispatch 函数,另一方面将新的 dispatch 初始的空函数替换为正常可执行的函数。同时由于 middlewareAPIdispatch 是以闭包形式引入的,当 dispatch 更新之后,闭包中的值也相应更新:

// 将 dispatch 替换为正常的 dispatch 方法
// 注意闭包中的值也会相应更新,middleware 可以访问到更新后的方法
dispatch = compose(...chain)(store.dispatch);

也就是说,createStore 生成的实例暴露的 dispatch 和 middleware 获取的都是修饰后的 dispatch ,并且应该是长这样:

function(action) {
  // 注意这里存在闭包
  // 可以获取到中间件初始化传入的 dispatch、getState 和 next
  // 如果你打断点,可以在 scope 中看到闭包的变量
  // 同时注意这里的 dispatch 就是这个函数本身
  console.info('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}

4. 处理异步 Action

由于 reducer 需要严格控制为纯函数,因此不能在里面进行异步操作,也不能进行网络请求。有些同学可能会说,虽然 reducer 里面不能放异步代码,但是可以把 dispatch 函数放在异步回调中调用呀:

setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

在 React 组件中通常用 connectdispatch 映射到组件的 props 中,类似 Vuex 中的 mapAction 用法。

确实可以!Redux 作者 Dan Abramov 在 Stackoverflow 上面有一个非常好的回答,其中就赞同了这种用法:

stackoverflow.com/questions/3…

本人将 Dan Abramov 的核心观点总结如下。

  • Redux 确实提供了一些处理异步 Action 的替代方法,但应该只在当你意识到你编写了大量模板代码的时候再去使用。否则就用最简单的方案(如无必要,勿增实体);

  • 当多个组件需要用到同一个 action.type 时,为避免 action.type 拼写错误,需要抽离公共的 actionCreator,例如:

    // actionCreator.js
    export function showNotification(text) {
      return { type: 'SHOW_NOTIFICATION', text }
    }
    export function hideNotification() {
      return { type: 'HIDE_NOTIFICATION' }
    }
    
    // component.js
    import { showNotification, hideNotification } from '../actionCreator'
    
    this.props.dispatch(showNotification('You just logged in.'))
    setTimeout(() => {
      this.props.dispatch(hideNotification())
    }, 5000)
    
  • 上面的逻辑在简单场景下完全可行,但是随着业务复杂度增加会出现几个问题:

    • 通常状态更新有好几个步骤,而且存在逻辑上的先后顺序,例如通知的展示和隐藏,导致模板代码较多;
    • 提交的 action 没有状态,如出现竞争条件可能导致状态更新出 bug ;
  • 出于上面的问题,需要抽离异步的 actionCreator ,把涉及状态更新的操作封装进去,便于复用,同时为每次 dispatch 生成唯一 id

    // actions.js
    function showNotification(id, text) {
      return { type: 'SHOW_NOTIFICATION', id, text }
    }
    function hideNotification(id) {
      return { type: 'HIDE_NOTIFICATION', id }
    }
    
    let nextNotificationId = 0
    export function showNotificationWithTimeout(dispatch, text) {
      const id = nextNotificationId++
      dispatch(showNotification(id, text))
    
      setTimeout(() => {
        dispatch(hideNotification(id))
      }, 5000)
    }
    

    然后在页面组件中这样使用,解决了模板代码和状态更新冲突问题:

    // component.js
    showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
    
    // otherComponent.js
    showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
    
  • 细心的同学应该注意到,这边传递了 dispatch 。这是因为,正常来说只有组件中能访问到 dispatch ,为了能让外部封装的函数也能访问,我们需要将 dispatch 作为参数传过去;

  • 这时有些同学会提出质疑,如果将 store 作为全局单例,不就可以直接访问了:

    // store.js
    export default createStore(reducer)
    
    // actions.js
    import store from './store'
    
    // ...
    
    let nextNotificationId = 0
    export function showNotificationWithTimeout(text) {
      const id = nextNotificationId++
      store.dispatch(showNotification(id, text))
    
      setTimeout(() => {
        store.dispatch(hideNotification(id))
      }, 5000)
    }
    
    // component.js
    showNotificationWithTimeout('You just logged in.')
    
    // otherComponent.js
    showNotificationWithTimeout('You just logged out.')
    
  • 上面这样从操作上来说确实可行,但是 Redux 团队并不赞同单例的写法。他们的理由是,如果 store 变为单例,会导致服务端渲染的实现变得困难,同时测试也不方便,如要改用 mock store 需要修改所有 import

  • 基于上面的原因,Redux 团队还是建议使用函数参数将 dispatch 传递出去,尽管这样很麻烦。那么有没有一种解决方案呢?有的,使用 Redux-thunk 就解决了这个问题;

  • 实际上,Redux-thunk 的作用是教 Redux 识别函数类型的特殊 Action ;

  • 中间件启用后,当 dispatch 的 Action 为函数类型,Redux-thunk 就会给这个函数传入 dispatch 作为参数,需要注意最终 reducer 拿到的仍然是普通 JavaScript 对象作为 Action :

    // actions.js
    
    function showNotification(id, text) {
      return { type: 'SHOW_NOTIFICATION', id, text }
    }
    function hideNotification(id) {
      return { type: 'HIDE_NOTIFICATION', id }
    }
    
    let nextNotificationId = 0
    export function showNotificationWithTimeout(text) {
      return function (dispatch) {
        const id = nextNotificationId++
        dispatch(showNotification(id, text))
    
        setTimeout(() => {
          dispatch(hideNotification(id))
        }, 5000)
      }
    }
    

    在组件中使用如下:

    // component.js
    this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
    

好了,Dan Abramov 的观点就总结到这里。

看到这里大家应该清楚 Redux-thunk 的作用了,Redux-thunk 本身并没有提供异步解决方案,实现异步就是使用最简单的方法,把 dispatch 函数放在异步回调中。很多时候我们会封装异步的 actionCreator ,在异步操作中每次都需要把 dispatch 传递出来很麻烦,Redux-thunkdispatch 函数进行高阶封装,允许接受函数类型的 Action ,同时给这个 Action 传入 dispatchgetState 作为参数,这样就不用每次手动传递。

在看源码之前,大家可以结合 applyMiddleware 源码,想一下 Redux-thunk 内部实现。

其实 Redux-thunk 实现原理非常简单,代码如下:

// src/index.ts:15
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) =>
    next =>
      action => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument)
        }

        return next(action)
      }
}

Redux-thunk 内部,首先会调用 createThunkMiddleware 方法得到一个高阶函数然后向外导出。这个函数就是我们之前分析的中间件结构:

({ dispatch, getState }) =>
  next =>
    action => {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument)
      }

      return next(action)
    }

首先在初始化阶段,applyMiddleware 会为 thunk 先后注入 middlewareAPI (对应 dispatchgetState 形参)和 store.dispatch (即原本的 dispatch 方法,对应 next 形参)。

在初始化完成之后,store 实例的 dispatch 会被替换为一个经过修饰的 dispatch 方法(middlewareAPI 中的 dispatch 由于是闭包引用,也会被替换),用 dispatch.toString() 打印可以输出如下内容:

// 注意这里可以访问到闭包中的 dispatch、getState 和 next
// 初始化完成后的 dispatch 实际上就是下面这个函数本身
action => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument)
  }

  return next(action)
}

接下来的事情就很简单了,当我们提交一个函数类型的 Action :

// actions.js
const setUserInfo = data => ({
  type: "SET_USER_INFO",
  payload: data
})

export const getUserInfoAction = userId => {
  return dispatch => {
    getUserInfo(userId)
      .then(res => {
        dispatch(setUserInfo(res));
      })
  }
}

// component.js
import { getUserInfoAction } from "./actionCreator";

this.props.dispatch(getUserInfoAction("666"));

当提交的 action 为函数类型的时候,就调用这个函数,然后传入 dispatchgetStateextraArgument 参数:

if (typeof action === 'function') {
  return action(dispatch, getState, extraArgument)
}

从这里可以看出,除了 dispatch 之外,在函数类型的 Action 内部还可以访问 getStateextraArgument

当异步操作完成,调用 Redux-thunk 传递的 dispatch 方法提交对象类型 Action 时,还是进入这个被修饰的 dispatch 方法,只不过在判断类型的时候,进入了另一个分支:

return next(action);

这里的 next 就是 Redux 原本的 dispatch 方法,会将对象类型的 Action 提交给 reducer 方法,最终执行状态更新。

5. 总结

Redux 是一种非常经典的状态管理解决方案。它遵循函数式编程的原则,状态只读且不可变,只有通过纯函数才能更新状态。

但是 Redux 同样也存在着不少问题。首先,对于新手来说,上手成本较高,使用之前需要先了解函数式编程的概念和设计思想。其次,Redux 在实际开发中非常繁琐,即使实现一个很简单的功能,可能也需要同时修改 4-5 个文件,降低了开发效率。作为对比,Vuex 的上手成本非常低,对于新手非常友好,使用也非常简单,既不需要异步中间件,也不需要额外的 UI binding ,在 Redux 中通过插件提供的功能,全部内置开箱即用。

对此,Redux 官方提供了一个封装方案 Redux Toolkit,社区也提供了很多封装方案,例如 Dva 、Rematch 等等,旨在简化 Redux 的使用,API 的封装上很多地方就是参考了 Vuex 。甚至还出现了酷似 Vue 响应式、使用可变数据(Mutable)的 Mobx 状态管理方案。此外,React 官方团队也在近期推出了 Recoil 状态管理库。

参考

redux.js.org

github.com/reduxjs/red…

github.com/reduxjs/red…

stackoverflow.com/questions/3…

coding优雅指南:函数式编程