写点什么

从 Redux 源码谈谈函数式编程

  • 2022 年 4 月 13 日
  • 本文字数:11553 字

    阅读完需:约 38 分钟

从 Redux 源码谈谈函数式编程

摘要

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


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


在 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 提交 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.jsconst 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.jsimport { 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.tsfunction 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:71if (  (typeof preloadedState === 'function' && typeof enhancer === 'function') ||  (typeof enhancer === 'function' && typeof arguments[3] === 'function')) {  // 传递两个函数类型参数的时候,抛出异常  // 也就是只接受一个 storeEnhancer  throw new Error();}
复制代码


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


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


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


// src/createStore.ts:87if (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.tsimport 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:46export 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 上面有一个非常好的回答,其中就赞同了这种用法:

https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559


本人将 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:15function 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.jsconst setUserInfo = data => ({  type: "SET_USER_INFO",  payload: data})
export const getUserInfoAction = userId => { return dispatch => { getUserInfo(userId) .then(res => { dispatch(setUserInfo(res)); }) }}
// component.jsimport { 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 状态管理库。

参考

https://redux.js.org

https://github.com/reduxjs/redux

https://github.com/reduxjs/redux-thunk

https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

coding优雅指南:函数式编程

发布于: 刚刚阅读数: 2
用户头像

高效学习,从有道开始 2021.03.10 加入

分享有道人的技术思考与实践。

评论

发布
暂无评论
从 Redux 源码谈谈函数式编程_JavaScript_有道技术团队_InfoQ写作平台