redux-thunk在异步中的用法以及redux-thunk的源码解析

600 阅读5分钟

主要是下记录一下 redux-thunk的学习,毕竟刚接触React以及React社区相关的产品

  • 看了一下redux-thunk的源码的实现,也非常的简单易懂
import type { Action, AnyAction } from 'redux'

import type { ThunkMiddleware } from './types'

export type {
  ThunkAction,
  ThunkDispatch,
  ThunkActionDispatch,
  ThunkMiddleware
} from './types'

/** A function that accepts a potential "extra argument" value to be injected later,
 * and returns an instance of the thunk middleware that uses that value
 */
function createThunkMiddleware<
  State = any,
  BasicAction extends Action = AnyAction,
  ExtraThunkArg = undefined
>(extraArgument?: ExtraThunkArg) {
  // Standard Redux middleware definition pattern:
  // See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
  const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
    ({ dispatch, getState }) =>
    next =>
    action => {
      // The thunk middleware looks for any functions that were passed to `store.dispatch`.
      // If this "action" is really a function, call it and return the result.
      if (typeof action === 'function') {
        // Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
        return action(dispatch, getState, extraArgument)
      }

      // Otherwise, pass the action down the middleware chain as usual
      return next(action)
    }
  return middleware
}

const thunk = createThunkMiddleware() as ThunkMiddleware & {
  withExtraArgument<
    ExtraThunkArg,
    State = any,
    BasicAction extends Action = AnyAction
  >(
    extraArgument: ExtraThunkArg
  ): ThunkMiddleware<State, BasicAction, ExtraThunkArg>
}

// Attach the factory function so users can create a customized version
// with whatever "extra arg" they want to inject into their thunks
thunk.withExtraArgument = createThunkMiddleware

export default thunk

  • 去掉TypeScript类型操作之后,其核心源码也更清晰了
const middleware = ({ dispatch, getState }) => next => action => {
      // The thunk middleware looks for any functions that were passed to `store.dispatch`.
      // If this "action" is really a function, call it and return the result.
      if (typeof action === 'function') {
        // Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
        return action(dispatch, getState, extraArgument)
      }

      // Otherwise, pass the action down the middleware chain as usual
      return next(action)
    }
  return middleware
  • 就是判断一下你传来的action是不是函数,是函数的话那就把这个函数执行了,所以这样我们就可以更优雅的写异步操作
  • 没有redux-thunk之前你写异步可能是这样写的
const dispatch = useDispatch()
const currentSong = useSelector(store => store.currentSong)

axios.get(api, {
    data: {
        id: currentSong.id
    }
}).then((response) => {
    dispatch(playMusic(response.id))
})
  • 有了redux-thunk之后你可以这样写
const dispatch = useDispatch()
const currentSong = useSelector(store => store.currentSong)

const asyncPlayMusic = (id) => (dispatch) => {
    return axios.get(api, {
        data: {
            id
        }
    }).then((response) => {
        dispatch(playMusic(response.id))
    })
}
function play() {
    dispatch(asyncPlayMusic(currentSong.id))
}
  • 下面是一个codesandboxredux-thunk写法的例子,可以自己在线操作感受一下,不得不说codesandbox很方便也很强大
import { useDispatch, useSelector } from 'react-redux'

import { addUser } from './store/test.slice'

import './styles.css'


export default function App() {

    const dispatch = useDispatch()

    const users = useSelector((store) => store.user)

    const aysncAddUser = (user) => async (dispatch) => {

        setTimeout(() => {

        return dispatch(addUser(user))

    }, 2000)

    }

    function add() {

        dispatch(aysncAddUser({ name: 'My', id: '7' }))

    }
    return (

        <div className="App">

            <ul>

                {users.map((user) => <li key={user.id}>{user.name}</li>}

            </ul>

        <button onClick={add}>Add</button>

        </div>

    )

}

codesandbox在线操作

题外话Thunk的含义

Thunk函数早在上个世纪60年代就诞生了。

那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。


var x = 1;

function f(m){
  return m * 2;     
}

f(x + 5)

上面代码先定义函数 f,然后向它传入表达式 x + 5 。请问,这个表达式应该何时求值?

一种意见是"传值调用"(call by value),即在进入函数体之前,就计算 x + 5 的值(等于6),再将这个值传入函数 f 。C语言就采用这种策略。

let y = x + 5
f(y)

另一种意见是"传名调用"(call by name),即直接将表达式 x + 5 传入函数体,只在用到它的时候求值。Hskell语言采用这种策略。


f(x + 5)

传值调用和传名调用,哪一种比较好?回答是各有利弊。 传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。


function f(a, b){
  return b;
}

f(3 * x * x - 2 * x - 1, x);

上面代码中,函数 f 的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。

因此,有一些计算机学家倾向于"传名调用",即只在执行时求值。

二、Thunk 函数的含义

编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。


function f(m){
  return m * 2;     
}

f(x + 5);

// 等同于

var thunk = function () {
  return x + 5;
};

function f(thunk){
  return thunk() * 2;
}

大致我们可以定义 Thunk 的具体含义如下:

Thunk 是一类函数的别名,主要特征是对另外一个函数添加了一些额外的操作,类似装饰器。其主要用途为延迟函数执行(惰性求值)或者给一个函数执行前后添加一些额外的操作。

// ThunkActionCreator
function fetchUser(id) {
  // 返回的这个函数既是一个 Thunk, 或者叫 ThunkAction
  return async (dispatch) => {
    // 额外的异步API调用
    const user = await api.getUser(id);
    // 此时才真正dispatch action
    dispatch({ type: 'UPDATE_USER', payload: user });
  }
}

给大家一个思考题fetchUser返回的异步函数里面的dispatch参数是怎么拿到的?也就是

async (dispatch) => {}

里的dispatch参数是怎么拿到的?可以看一下上面的redux-thunk源码

再来分析一下redux-thunk的核心源码

const middleware = ({ dispatch, getState }) => next => action => {
      // The thunk middleware looks for any functions that were passed to `store.dispatch`.
      // If this "action" is really a function, call it and return the result.
      if (typeof action === 'function') {
        // Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
        return action(dispatch, getState, extraArgument)
      }

      // Otherwise, pass the action down the middleware chain as usual
      return next(action)
    }
  return middleware

当我执行

function fetchUser(id) {
  return async function thunk(dispatch) => {
      setTimeout(() => {
          dispatch(updateUser(id))
      }, 2000)
  }
}
dispatch(fetchUser(id))

redux-thunk中间件会分析到

typeof action === 'function'// === true

即把action看作thunk这个函数

typeof thunk === 'function' // === true

而后redux-thunk到了下面这个逻辑

return action(dispatch, getState, extraArgument)

等价于

thunk(dispatch, getState, extraArgument)

最后thunk函数里面的dispatch参数来源的问题就解决了,然后我们就能这么写了

function fetchUser(id) {
  return async function thunk(dispatch) => {
      setTimeout(() => {
          dispatch(updateUser(id))
      }, 2000)
  }
}
dispatch(fetchUser(id))