Redux 源码学习

Redux 和 Mobx 是 React 常用的状态管理库. 简单的说, Mobx 是把数据变为可观察, 每个数据在引用的时候都会建立依赖, 当数据变化时, 会去触发依赖高效的进行更新. Redux 则手动管理, 通过 dispatch action 去触发 reducer 变更 state, React 接受新的 state 后更新视图, 一切都自己管理, 所以说是可预测.

我最早接触的 Redux, 不过并没有给我太多好感, 因为感觉写起来太麻烦了, 那时候我还在写 AngularJS, 无法理解 AngularJS 轻轻松松搞定的事情, 非要用 React 写的老复杂了. 后来接触了 Mobx, 但是也没明白它和 React 怎么配合的, 但是仔细研究之后, 发现它和 Vuex 很类似. 加上我习惯用 TypeScript, Mobx 的装饰器写起来非常简洁, 所以在项目中基本用的都是 Mobx.

不过 Redux 总不能视而不见的, 虽然 Redux 的用法还是大概明白, 但是并不明白它底层是怎么工作的. 今天就研究了下 Redux 源码, 打开黑箱子看看. Redux 的源码非常简单也非常少. 还是很容易看的. 推荐每个人都去看下, 这里只是说几个我通过看 Redux 源码搞明白的三个问题.

dispatch 之后发生了什么

我们直接 dispatch 一个 action 之后, reducer 就会获得最新的 state 以及 action 函数返回的对象, 然后我们返回新的或者原来的 state 之后, React 就会重新渲染. 我们暂时不关心 React 怎么知道需要去重新渲染的? 我们先来看看 reducer 是如何被调用的?

首先我们要明白, 看似我们写了很多 reducer, 但是在 createStore 方法中, 只接受一个 rootReducer, 一般我们是使用 combineReducers 方法返回了一个 rootReducer. 当我们 dispatch 一个 action 时, redux 会把当前 rootState 和 action 对象传给 rootReducer 去执行, 我们需要在 rootReducer 中返回一个新的 rootState 出来, 之后 redux 会通知全部的 listeners.

但一般我们不会这么做, 因为一个单一 reducer 和单一 state 无疑会迅速使的代码变得混乱. 因此我们一半会用 combineReducers 方法. 例如:

const rootReducer = combineReducers({
  book: bookReducer,
  order: orderReducer
})

有一点要求就是我们的 state 应该匹配它. 什么意思呢, 既然我们把 reducer 划分成多个, 自然我们也想相对应的把 state 划分为同样的多个, 并一一对应到相应的 reducer 去, 即该 reducer 只接受其需要的那部分 state.

所以 state 应该类似的:

const rootState = {
  book: bookState
  order: orderState
}

这样做之后, 以后我们再 dispatch 时, redux 就会把当前 rootState 和 action 给我们 combine 之后的 rootReducer 去执行, rootReducer 会按照 key 一一对应把 state[key] 和 action 传给 reducers[key] 去执行. 也就是说, 所有的 reducer 同样会被执行, 只不过大多都因为不满足我们 action 中的 type 而返回了 default 值即原 state[key]. 这就是为什么要求 type 唯一并且要求无需处理时返回原 state 的原因.

全部 reducer 执行后, 如果其中有一个 state 和原传入的 state 不同, 则返回合并之后的新 state, 否则返回原来的 state, 避免不必要的更新.

为什么需要 redux-xxx

在使用 redux 的项目中, 也经常会使用一些 redux 的中间件, 像 redux-thunk, redux-promise, redux-saga 等等. 为什么会需要这些? redux 的中间件有什么作用?

如果只看 redux 源码的话, 会发现 dispatch 只接收一个 action 对象, 该对象必须有一个 type. actionCreator 总是一个纯函数, 返回一个 action 对象.

但如果我们需要一些异步的操作, 例如获取服务器上的资源, 然后再更新到 state 上, 该怎么操作呢? 如果我们不使用中间件, 我们可能需要这么做.

async componentWillMount () {
  try {
    // 准备获取数据, 比如设置加载中动画, 或者进行预更新
    store.dispatch({ type: 'GET_DATA_PRE' })
    const data = await axios.get(`/book/${page}`)
    // 设置数据
    store.dispatch({ type: 'GET_DATA_SUCCESS', payload: data }) 
  } catch (e) {
    // 数据获取失败处理, 可以撤销预更新处理
    store.dispatch({ type: 'GET_DATA_ERROR', payload: e.message }) 
  } finally {
    // 完成获取, 可以结束加载中动画
    store.dispatch({ type: 'GET_DATA_AFTER' }) 
  }
}

如果我们在另一个页面也需要进行同样操作, 代码是不是要重复写两次呢? 上面是一个获取 data 的一个过程, 相对来说还是比较简单的, 有时候业务比较复杂上面这个操作的代码可能会很多, 会有各种 dispatch.

所以从复用的角度, 我们需要把其封装为一个操作来执行. 如果把其转为一个普通的异步函数的话, 也不是不行, 如上全部操作如果封装为一个函数:

function fetchBookList(dispatch, page) {
  try {
    // 准备获取数据, 比如设置加载中动画, 或者进行预更新
    store.dispatch({ type: 'GET_DATA_PRE' })
    const data = await axios.get(`/book/${page}`)
    // 设置数据
    store.dispatch({ type: 'GET_DATA_SUCCESS', payload: data }) 
  } catch (e) {
    // 数据获取失败处理, 可以撤销预更新处理
    store.dispatch({ type: 'GET_DATA_ERROR', payload: e.message }) 
  } finally {
    // 完成获取, 可以结束加载中动画
    store.dispatch({ type: 'GET_DATA_AFTER' }) 
  }
}
async componentWillMount () {
  await fetchBookList(store.dispatch, 1)
}

如果我们使用 redux-thunk, 那么代码可以变成这样

function fetchBookList(page) {
  return async function (dispatch, getState) {
    try {
      // 准备获取数据, 比如设置加载中动画, 或者进行预更新
      dispatch({ type: 'GET_DATA_PRE' })
      const data = await axios.get(`/book/${page}`)
      // 设置数据
      dispatch({ type: 'GET_DATA_SUCCESS', payload: data }) 
    } catch (e) {
      // 数据获取失败处理, 可以撤销预更新处理
      dispatch({ type: 'GET_DATA_ERROR', payload: e.message }) 
    } finally {
      // 完成获取, 可以结束加载中动画
      dispatch({ type: 'GET_DATA_AFTER' }) 
    }
  }
}
componentWillMount () {
  store.dispatch(fetchData(1))
}

无疑后面这种更加清晰和易读. 但也有个问题, 我们无法准确把握 state 被更新的时机. 因为 store.dispatch 操作是同步的.

其实如果看明白了 redux 代码的 applyMiddleware 方法, 结合 redux-thunk 源码(只有几行), 就能明白了. 通过 applyMiddleware 方法可以改写 store.dispatch 方法, 让其可以接收函数(thunk), 甚至是 Promise 和生成器. 即对应上文所说的 redux-thunk, redux-promise, redux-saga 三个中间件.

redux-thunk 其实代码关键的就这几行:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      // dispatch 相当于 (...args) => next(...args), 这样写目的应该是为了避免 store.dispatch 被修改
      return action(dispatch, getState, extraArgument) 
    }

    return next(action) // next 是原 store.dispatch 操作
  }
}

不过这里有一个地方需要注意, redux-thunk 依赖的 redux 版本比较旧, 旧到其实是不能兼容的. 所以看 redux-thunk 的源码的话, 结合其依赖版本的 redux 看比较好. 当然了, redux 只要看 applyMiddleware 这个方法就好了.

redux-thunk 和 redux-promise, 以及 redux-saga 是比较有名的三个异步方案. 晚点再仔细研究下他们的异同和优缺点. 这里只是想说下 redux 的 middleware 而已.

Redux 如何搭配 React 使用

通过 Redux 的源码, 我们可以 store.subscriber 来监听 state 变化. 当我们 dispatch 一个事件后, Redux 会通知全部的 listener. 此时我们可以获取到新的 state, 然后判断是否变化来触发 React 去进行更新操作. 类似下面这样:

constructor () {
  super()
  this.state = store.getState()
  store.subscribe(() => {
    this.setState(store.getState())
  })
}

这样做有很多问题, 最大的问题是 subscribe 方法监听的是 dispatch 方法的调用, 当 dispatch 被调用后, 所有的 listener 都会被执行, 我们需要去手动判断当前组件所需要的 state 是否真的变化了, 然后去触发更新. 另外, 出于性能考虑, 我们应该在 componentWillUnmount 中取消 subscribe, 这带来了更多的工作.

所以, 一般在 React 项目中使用 Redux, 也会对应的使用 react-redux 这个库. 它提供了 Provider 和 connect 两个关键的 API. react-redux 的 connect 进行了大量的优化, 减少了组件无必要的渲染.

For example, components generated by connect() bail out of rendering if they see that the props returned from mapStateToProps() are shallowly equal to their previous versions.

Have a look at connect() source code. You will see that the idea is the same, but it has a ton of
optimizations at different points. In general, it avoids calling your functions or rendering unless absolutely necessary.

react-redux 的源码还没看, 不过简单看了下也, 代码也不多, 主要是引入了 connectProvider, 并进行了大量优化, 可以提高效率. 晚点再详细看看. 这篇文章主要还是讲阅读 redux 源码解决的我心中的三个问题.


参考链接: