Talk about functional programming from Redux source code

Summary

In the world of React, there are hundreds of state management solutions, but the most classic of them is Redux. If you want to learn functional programming, then Redux source code is the best learning material. Considering that many small partners use Vue, I strive to make this article easy to understand, so that students who have not been exposed to the React technology stack can also master Redux.

In the world of React, there are hundreds of state management solutions, but the most classic of them is Redux. If you want to learn functional programming, then Redux source code is the best learning material. Considering that many small partners use Vue, I strive to make this article easy to understand, so that students who have not been exposed to the React technology stack can also master Redux.

In the world of React, there are hundreds of state management solutions, but the most classic of them is Redux. If you want to learn functional programming, then Redux source code is the best learning material. Considering that many small partners use Vue, I strive to make this article easy to understand, so that students who have not been exposed to the React technology stack can also master Redux.

Redux belongs to the typical "hundred lines of code, thousands of lines of documents", in which the core code is very small, but the idea is not simple, which can be summarized as the following two points:

  • The global state is unique and immutable (Immutable). Immutable means that when the state needs to be modified, it is replaced with a new one, rather than directly changing the original data:
 let store = { foo: 1, bar: 2 };

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

This is just the opposite of Vue. In Vue, the original object must be modified directly in order to be monitored by the reactive mechanism, thereby triggering the setter to notify the dependency update.

The state update is done through a pure function (Reducer). Pure functions are characterized by:

  • The output is only related to the input;
  • Reference is transparent and does not depend on external variables;
  • No side effects;

Therefore, for a pure function, the same input must produce the same output, which is very stable. Use pure functions to modify the global state so that the global state can be predicted.

1. Several concepts to understand

Before using Redux and reading the source code, you need to understand the following concepts:

Action

action is a plain JavaScript object describing how to modify the state, which needs to contain a type attribute. A typical action looks like this:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}
复制代码

Reducers

A reducer is a pure function with the following function signature:

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

The name of the reducer functions comes from the reduce method of the array, because they are similar to the callback function passed by the array reduce method, that is, the value returned by the previous call will be passed as the parameter of the next call.

The writing of reducer functions needs to strictly follow the following rules:

  • Check if the reducer cares about the current action
  • If so, create a copy of the state, update the state in the copy with the new value, and return the copy
  • Otherwise, return to the current state

A typical reducer function is as follows:

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 提交 action 。store 将会调用 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 实例会暴露两个方法 getState 和 dispatch ,其中 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 的实现其实非常简单,整体就是一个闭包环境,里面缓存了 currentReducer 和 currentState ,并且定义了getState、subscribe、dispatch 等方法。

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 方法,传入 reducer 和 preloadState 。preloadState 代表初始状态,假如不传那么 reducer 必须要指定初始值;
  • 将 reducer 和 preloadState 分别赋值给 currentReducer 和 currentState 用于创建闭包;
  • 创建 listeners 数组,这其实就是基于发布订阅模式,listeners 就是发布订阅模式的事件中心,也是通过闭包缓存;
  • 创建 getState 、subscribe 、dispatch 等函数;
  • 调用 dispatch 函数,提交一个 INIT 的 action 用来生成初始state,在 Redux 源码中,这里的 type 是一个随机数;
  • 最后返回一个包含 getState 、subscribe 、dispatch 函数的对象,即 store 实例;

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

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

当需要更新状态时,调用 dispatch 提交 action 。在 dispatch 函数中调用 currentReducer(也就是 reducer 函数),并传入 currentState 和 action ,然后生成一个新的状态,传给 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 ,分两次传入 createStore 和 reducer 、preloadedState ;
  • 内部再次调用 createStore ,这次由于没有传 enhancer ,所以直接走创建 store 的流程;
  • 创建一个经过修饰的 dispatch 方法,覆盖默认 dispatch ;
  • 构造 middlewareAPI ,对 middleware 注入 middlewareAPI ;
  • 将 middleware 实例组合为一个函数,再向 middleware 传递默认的 store.dispatch 方法;
  • 最后返回一个新的 store 实例,此时 store 的 dispatch 方法经过了 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 的时候调用 dispatch ,applyMiddleware 给新的 dispatch 初始化为一个空函数,且调用会抛出异常,那么这个函数究竟在何时被替换掉的;

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

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

// 定义新的 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 初始的空函数替换为正常可执行的函数。同时由于 middlewareAPI 的 dispatch 是以闭包形式引入的,当 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 组件中通常用 connect 把 dispatch 映射到组件的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)
复制代码
  • 上面的逻辑在简单场景下完全可行,但是随着业务复杂度增加会出现几个问题:
  1. 通常状态更新有好几个步骤,而且存在逻辑上的先后顺序,例如通知的展示和隐藏,导致模板代码较多;
  2. 提交的 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-thunk 对 dispatch 函数进行高阶封装,允许接受函数类型的 Action ,同时给这个 Action 传入 dispatch 和 getState 作为参数,这样就不用每次手动传递。

在看源码之前,大家可以结合 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 (对应 dispatch 和 getState 形参)和 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 为函数类型的时候,就调用这个函数,然后传入 dispatch 、getState 、extraArgument 参数:

if (typeof action === 'function') {
  return action(dispatch, getState, extraArgument)
}
复制代码

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

当异步操作完成,调用 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优雅指南:函数式编程

Guess you like

Origin juejin.im/post/7086037825115652104