Redux中的小细节

avatar
@https://www.tuya.com/

作者:涂鸦-李青

来涂鸦工作: job.tuya.com/


isDispatching

Redux源码里的createStore方法维护了一个isDispatching变量,表示dispatch的状态,当调用dispatch时会将该变量赋值为true,详细代码为

let isDispatching = false

function dispatch(action: A) {
    // ...
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // ...

    return action
}

Redux需要isDispatching做什么,还在哪里使用了它?在源码中搜索这个变量,发现除了dispatch函数 本身使用了isDispatching,还在getStatesubscribeunsubscribe方法中发现同样消费了isDispatching,其逻辑相似,即isDispatchingtrue时抛出错误。代码为

// subscribe  unsubscribe
function subscribe(listener: () => void) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api/store#subscribelistener for more details.'
      )
    }
  
   // ...
  
  return function unsubscribe() {
      // ...
      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api/store#subscribelistener for more details.'
        )
      }
      // ...
    }
}

// getState
function getState(): S {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState as S
}

看完这两部分代码会让人疑惑,isDispatching好像没什么作用,我们知道js是单线程语言,并且dispatchgetStatesubscribeunsubscribe都是同步函数,既然是同步场景,我们在调用dispatch时,js会执行完这个函数再处理其他函数,不存在dispatchgetStatesubscribeunsubscribe同时执行的情况。但问题就出现在dispatch这个函数本身,我们再仔细观察dispatch核心实现。

function dispatch(action: A) {
  // ...
  try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
}

dispatchaction交给reducer处理,这里的currentReducer就是createStore传入的reducer,即开发者编写的reducer。假如有这样一个reducer

const reducer = (state={}, action) => {
  store.dispatch({type: 'SOMEACTION'})
  return state
}

这个reducer内部又调用dispatch方法,若没有isDispatchingdispatch执行时会发生dispatch -> reducer -> dispatch -> reducer无限循环调用导致堆栈溢出。因此Redux认为外界传入的reducer是不安全的,便通过isDispatching来限制reducer

currentListeners和nextListeners

Redux实现了订阅-监听-发布功能,通过subscribe方法订阅添加一个监听者listener,每一次调用dispatchRedux会遍历通知所有的监听器。这个功能正常代码实现方式是:

// 简易代码
const listeners = []

function subscribe(listener) {
  listeners.push(listener)
  
  return function unsubscribe() {
    const index = listeners.indexOf(listener)
    listeners.splice(index, 1)
  }
}

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

Redux内部维护了currentListenersnextListeners两个监听器数据源,其代码为

let currentListeners = []
let nextListeners = currentListeners

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
}

function subscribe(listener) {
		// ...
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      // ...
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
}

function dispatch(action) {
    // ...
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
}

实际上subscribe方法接收的是外部传进来的监听器,若监听器中又执行了subscribeunsubscribe方法,则会造成监听器执行的混乱甚至出错。比如这样订阅:

function loopSubscribe () {
  store.subscribe(loopSubscribe)
}

loopSubscribe()

store.dispatch()

此时只维护一个监听器变量的代码会造成死循环,为了防止传入的listener又调用subscribe添加监听器,Redux使用了两个变量来维护监听器,凡是涉及到操作listener的方法即subscribeunsubscribe都是操作nextListeners数据, dispatch时将nextListeners合并到currentListeners,遍历通知所有的currentListeners。换句话说,dispatch会进行一次监听器快照,这时如果监听器listener又执行了subscribeunsubscribe方法,只会在下一次dispatch方法执行时生效。

applyMiddleware

Redux生态离不开中间件,其中redux-thunk就是众多中间件的一个,其代码为:

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

    return next(action);
  };
}

redux-thunk看起来相对复杂,依次返回了三层函数,我们来看Redux中间件核心函数applyMiddleware如何处理中间件,

function applyMiddleware(
  ...middlewares
) {
  return 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: (action, ...args) => dispatch(action, ...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

Redux会对中间件的三层函数依次解开,首先第一层Redux遍历所有的中间件并赋予了中间件middlewareAPI的能力,middlewareAPI中有一个dispatch方法,但此时的dispatch并非原来store中的dispatch,它随后会被修改。

此时中间件被交给compose进行链式组合,其结果就是给每个中间件增加了调用下一个中间件的能力(next),这是第二层。

compose的实现方式也很巧妙:

function compose(...funcs) {
  if (funcs.length === 0) {
    // infer the argument type so it is usable in inference down the line
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

假如有两个中间件a, b,则compose结果为**(...args) => a(b(...args))**,compose返回结果又传入了store.dispatch,即argsstore.dispatch,这样中间件a可以调用next执行中间件b,而中间件b调用nextstore.dispatch

最后一层就是开发者调用dispatchaction传入,即第三层函数能够访问action对象。

compose后的dispatch为增强版的dispatch,注意,最后一个中间件调用next是在调用store.dispatch,即未增强的dispatch

结束

Redux目前虽然有点「过时」,但其中的设计和细节仍然有借鉴之处,如isDispatching监听器快照等实现帮助我们产出更加健壮的代码。


来涂鸦工作: job.tuya.com/