逐行阅读redux源码(三)bindActionCreators & applyMiddleware & compose

537 阅读8分钟

前情提要

ps: 因为余下的源码皆是短篇幅,于是一并合为一文进行书写,如有不足之处望各位指正。

bindActionCreators

image

什么是actionCreators

相信大家在阅读了之前的文章之后,对什么是action已经很熟悉了,我们的action其实就是一个object,通常其中会包含一个type参数和一个数据载体payload参数,用来给redux在使用dispatch更新状态树时指一条明路。

那么什么是actionCreators呢?

那么从字面而言,actionCreators可以理解为:动作创建器,也就是说我们的actionCreators是用来生成action(动作)的。我们在业务开发的时候,通常会使用很多不同的action,但是这些action很多时候都会包括一个不定变量,比如我们在更新一个列表的时候,我们可能会使用这样一个action:

async function getListData() {...}
await const listData = getListData();

dispatch({
    type: 'updateList',
    data: listData
});

虽然写一个匿名的action也ok,但是在大型项目这样写的话维护将存在极大问题的,你根本就不知道这个action有什么作用(大部分情况下),所以我们在统一管理这类action时通常使用actionCreators(当然也为了让action变得更加灵活):

// actionCreator通常为纯函数
function updateList(listData) {
    return {
        type: 'updateList',
        data: listData
    }
}

为什么需要bindActionCreators

我们在书写页面组件的时候,如果要用react-redux更新我们的状态树然后重渲染页面,通常会使用redux绑定在页面的props上的dispatch方法触发action

当我们的页面上不存在子组件,或者子组件中不需要使用redux时这样做是没有任何问题的,但是当我们需要在子组件中使用dispatch时,我们就会发现,子组件的Props是纯净的,没有任何地方可以调用dispatch

当然你也可以把父组件中的dispatch方法通过props传递到子组件中使用,但是正确的操作,还是使用我们的bindActionCreators

我们可以将我们需要使用的actionCreators封装至一个对象中,key值就是actionCreators的函数名,值就是对应其函数:

function action1(param){...}
function action2(param){...}

const actionCreators = {
    action1,
    action2
}

然后我们使用bindActionCreators:

const { dispatch } = this.props;

const dispatchActionsObject = bindActionCreators(actionCreators, dispatch);
//dispatchActionsObject为:
{
    action1: (param) => {dispatch(action1(param))},
    action2: (param) => {dispatch(action2(param))}
}

我们只需要将dispatchActionsObject通过props绑定到子组件上,那么子组件就可以在不知道是否存在redux的情况下使用我们提供的函数来更新状态树了。

// 父组件
...
class Parent extends React.Component {
    ...
    render(
        <Parent>
            <Child {...dispatchActionsObject}></Child>
        </Parent>
    )
}


// 子组件
class Child extends React.Component {
  ...
  doAction1(data) {
      this.props.action1(data)
  },
  doAction2(data) {
      this.props.action2(data)
  }
}

源码

bindActionCreator
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

bindActionCreators的开始部分,我们会发现这个方法bindActionCreator接受了两个参数,一个actionCreator,一个dispatch

随后其作为高阶函数,返回了一个新的匿名函数,并在匿名函数中执行了dispatch操作,使用Function.prototype.apply,接受了外部的传参值,并传入actionCreator中生成action

所以这个函数的功能就是:将传入的单个actionCreator封装成dispatch这个actionCreator的函数。 例如:

const actionCreator(data) {
    return {
        type: 'create',
        data
    }
};

const dispatch = this.props.dispatch;

const dispatchActionCreatorFn = bindActionCreator(actionCreator, dispatch);
// 这个方法会返回一个function
// actionObj = (data) => {
//    dispatch(actionCreator(data);
// }
bindActionCreators

我们的bindActionCreatorsbindActionCreator入参的差别就在于第一个参数,bindActionCreator接受的是一个function,而bindActionCreators则接受的是function或者object

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }
  ...
}  

从源码上看,我们的bindActionCreators做了一些简单的防错和兼容处理,当接受的是function时,其作用和直接bindActionCreator是一致的,都是返回一个隐式调用dispatch的方法,而当其传入的是一个object时,会强制阻止用户使用null为参数传入。

const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
  boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators

在防错处理之后,便是我们整个bindActionCreators的核心部分。我们会通过Object.keys()遍历获取actionCreators对象的key值数组,并声明一个空对象boundActionCreators准备用来存放我们即将生成的隐式调用dispatch的方法。

其后我们通过遍历key值数组,将每个key值对应的actionCreator取出,简单的判断actionCreator是否合规,然后使用bindActionCreator生成对应的匿名函数,并存放在我们之前声明的boundActionCreators的同key值之下,这之后,我们的bindActionCreators完成了~

compose

因为applyMiddleware依赖于函数式编程的compose(组合),我们先从compose入手吧~

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)))
}

compose是个非常简单的纯函数,其作用是通过reduce将传入的方法按照从右到左的顺序组合起来。

实现过程让我们一步一步来尝试:

ps: 希望你在阅读compose源码之前先了解rest箭头函数

  • 首先是一些兼容性处理,避免函数报错
  • 使用reduce遍历数组中的function,我们可以用简单的例子来看看到底发生了什么
function addOne(x) {
    return x+1;
}

function double(x) {
    return x*2;
}

function println(x) {
    retrun x;
}

const funcs = [println, double, addOne];

当我们第一次reduce的时候:

// 因为不存在 `initialValue` 
// 所以第一次reduce的第一个参数可以视作`currentValue`
// 第二个参数视作`nextValue`

[println, double, addOne].reduce((a,b) => {
    return (...args) => {a(b(...args))}
})

// 得出的结果是
[println, double, addOne].reduce((a,b) => {
    return (...args) => {print(double(...args))}
})

此时我们的reduce可以看作是:

[ (...args) => {print(double(...args))}, addOne].reduce((a,b) => {...})

因为此时reduce回调中的第一个参数会变成上一次遍历时的返回值,所以接下来的reduce会变成这样:

[ (...args) => {print(double(...args))}, addOne].reduce((a,b) => {
    // 此时 a = (...args) => {print(double(...args));
    // b = addOne
    // 所以 a(b(..args)) = print(double(addOne(...args)))
    return (...args) => {
        print(double(addOne(..args)))
    }
})

由此可以看出,我们的compose虽然简单,但是实现的功能确是很强大的,其像洋葱一样,一层一层的向外调用,每一个函数的结果都作为上外层函数的入参被调用,直到最后得出结果。

image

applyMiddleware

image

在说applyMiddlewares之前,让我们先回忆一下我们的createStore中传入了enhancer后的处理过程:

if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function'
    )
  }

  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.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

可以看到,当createStore接收到可用的enhancer时,会将creteStore作为参数传入高阶函数enhancer中,并使用之前传入的reducer/preloadedState继续进行创建store.

而实现enhancer的方法,就是applyMiddleware.

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

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

    return {
      ...store,
      dispatch
    }
  }
}

可以从源码看到,我们的applyMiddleware返回的便是一个柯里化的高阶函数,其接受入参的顺序也符合enhancer在被调用时的顺序,所以我们不妨直接把...args直接替换成reducer, preloadedState.此时,我们的createStore会变成这样:

const store = createStore(reducer, preloadedState)
let dispatch = () => {
  throw new Error(
    `Dispatching while constructing your middleware is not allowed. ` +
      `Other middleware would not be applied to this dispatch.`
  )
}

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

return {
  ...store,
  dispatch
}

接下来会创建一个抛错的dispatch,阻止用户在没有处理完middleware时调用真正的dispatch影响状态树导致中间件middleware不能获取实时的状态值。

接下来,其创建了一个middlewareAPI,用来存放当前store的状态值和之前会抛错的dispatch,并作为参数,结合middlewares生成一组匿名函数,每个匿名函数的返回值都是通过当前middleware处理了middlewareAPI的返回值,之后通过compose,将这些匿名函数组合,让其能够从右到左的链式调用,最后再为组合生成的函数传入store.dispatch(真正的disaptch)作为参数值,其过程如下:

// 假设我有两个middleware: mdw1, mdw2
// compose过程如下:
// 而mdw1, mdw2经过遍历是如下的数组
[mdw1(middlewareAPI), mdw2(middlwwareAPI)]
// 经过compose后
return (...args) => mdw1(middlewareAPI)(mdw2(middlwwareAPI)(...args))

// 传入store.dispatch之后
return mdw1(middlewareAPI)(mdw2(middlwwareAPI)(store.dispatch))

这样处理之后,我们通过链式调用中间件处理完成的dispatch才能正式return出去,作为store的一部分。

结语

本文至此,redux的5个文件的源码也算是告一段落,从阅读redux的源码中,学到了很多关于函数式编程的思路和一些技巧,以及对各种边界情况的重视。以前并不注重其中实现,觉得让自己来也能写出来,但是真到了自己看的时候才发现,原来还是图样图森破,也只有感叹一句:任重道远啊...

最后,感谢你的阅读~