资讯专栏INFORMATION COLUMN

解密Redux: 从源码开始

remcarpediem / 2572人阅读

摘要:接下来笔者就从源码中探寻是如何实现的。其实很简单,可以简单理解为一个约束了特定规则并且包括了一些特殊概念的的发布订阅器。新旧中存在的任何都将收到先前的状态。这有效地使用来自旧状态树的任何相关数据填充新状态树。

Redux是当今比较流行的状态管理库,它不依赖于任何的框架,并且配合着react-redux的使用,Redux在很多公司的React项目中起到了举足轻重的作用。接下来笔者就从源码中探寻Redux是如何实现的。

注意:本文不去过多的讲解Redux的使用方法,更多的使用方法和最佳实践请移步Redux官网。
源码之前 基础概念

随着我们项目的复杂,项目中的状态就变得难以维护起来,这些状态在什么时候,处于什么原因,怎样变化的我们就很难去控制。因此我们考虑在项目中引入诸如Redux、Mobx这样的状态管理工具。

Redux其实很简单,可以简单理解为一个约束了特定规则并且包括了一些特殊概念的的发布订阅器。

在Redux中,我们用一个store来管理一个一个的state。当我们想要去修改一个state的时候,我们需要去发起一个action,这个action告诉Redux发生了哪个动作,但是action不能够去直接修改store里头的state,他需要借助reducer来描述这个行为,reducer接受state和action,来返回新的state。

三大原则

在Redux中有三大原则:

单一数据源:所有的state都存储在一个对象中,并且这个对象只存在于唯一的store中;

state只读性:唯一改变state的方法就是去触发一个action,action用来描述发生了哪个行为;

使用纯函数来执行修改:reducer描述了action如何去修改state,reducer必须是一个纯函数,同样的输入必须有同样的输出;

剖析源码 项目结构

抛去一些项目的配置文件和其他,Redux的源码其实很少很简单:

index.js:入口文件,导出另外几个核心函数;

createStore.js:store相关的核心代码逻辑,本质是一个发布订阅器;

combineReducers.js:用来合并多个reducer到一个root reducer的相关逻辑;

bindActionCreators.js:用来自动dispatch的一个方法;

applyMiddleware.js:用来处理使用的中间件;

compose.js:导出一个通过从右到左组合参数函数获得的函数;

utils:两个个工具函数和一个系统注册的actionType;

从createStore来讲一个store的创建

首先我们先通过createStore函数的入参和返回值来简要理解它的功能:

export default function createStore(reducer, preloadedState, enhancer) {

  // ...

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

createStore接受三个参数:

reducer:用来描述action如何改变state的方法,它给定当前state和要处理的action,返回下一个state;

preloadedState:顾名思义就是初始化的state;

enhancer:可以直译为增强器,用它来增强store的第三方功能,Redux附带的唯一store增强器是applyMiddleware

createStore返回一个对象,对象中包含使用store的基本函数:

dispatch:用于action的分发;

subscribe:订阅器,他将会在每次action被dispatch的时候调用;

getState:获取store中的state值;

replaceReducer:替换reducer的相关逻辑;

接下来我们来看看createStore的核心逻辑,这里我省略了一些简单的警告和判断逻辑:

export default function createStore(reducer, preloadedState, enhancer) {
  // 判断是不是传入了过多的enhancer
  // ...

  // 如果不传入preloadedState只传入enhancer可以写成,const store = createStore(reducers, enhancer)
  // ...

  // 通过在增强器传入createStore来增强store的基本功能,其他传入的参数作为返回的高阶函数参数传入;
  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.")
  }

  // 闭包内的变量;
  // state作为内部变量不对外暴露,保持“只读”性,仅通过reducer去修改
  let currentReducer = reducer
  let currentState = preloadedState
  // 确保我们所操作的listener列表不是原始的listener列表,仅是他的一个副本;
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  // 确保我们所操作的listener列表不是原始的listener列表,仅是他的一个副本;
  // 只有在dispatch的时候,才会去将currentListeners和nextListeners更新成一个;
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 通过闭包返回了state,state仅可以通过此方法访问;
  function getState() {
    // 判断当前是否在dispatch过程中
    // ...

    return currentState
  }

  // Redux内部的发布订阅器
  function subscribe(listener) {
    // 判断listener的合法性
    // ...

    // 判断当前是否在dispatch过程中
    // ...

    let isSubscribed = true

    // 复制一份当前的listener副本
    // 操作的都是副本而不是源数据
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      // 判断当前是否在dispatch过程中
      // ...

      isSubscribed = false

      ensureCanMutateNextListeners()

      // 根据当前listener的索引从listener数组中删除来实现取掉订阅;
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    // 判断action是不是一个普通对象;
    // ...

    // 判断action的type是否合法
    // ...

    // 判断当前是否在dispatch过程中
    // ...

    try {
      isDispatching = true
      // 根据要触发的action, 通过reducer来更新当前的state;
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 通知listener执行对应的操作;
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  // 替换reducer,修改state变化的逻辑
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== "function") {
      throw new Error("Expected the nextReducer to be a function.")
    }

    currentReducer = nextReducer

    // 此操作对ActionTypes.INIT具有类似的效果。
    // 新旧rootReducer中存在的任何reducer都将收到先前的状态。
    // 这有效地使用来自旧状态树的任何相关数据填充新状态树。
    dispatch({ type: ActionTypes.REPLACE })
  }

  function observable() {
    const outerSubscribe = subscribe
    return {
      // 任何对象都可以被用作observer,observer对象应该有一个next方法
      subscribe(observer) {
        if (typeof observer !== "object" || observer === null) {
          throw new TypeError("Expected the observer to be an object.")
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        // 返回一个带有unsubscribe方法的对象可以被用来在store中取消订阅
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

  // 创建store时,将调度“INIT”操作,以便每个reducer返回其初始状态,以便state的初始化。
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
从combineReducers谈store的唯一性

仅靠上面的createStore其实已经可以完成一个简单的状态管理了,但是随着业务体量的增大,state、action、reducer也会随之增大,我们不可能把所有的东西都塞到一个reducer里,最好是划分成不同的reducer来处理不同模块的业务。

但是也不能创建多个store维护各自的reducer,这就违背了Redux的单一store原则。为此,Redux提供了combineReducers让我们将按照业务模块划分的reducer合成一个rootReducer。

接下来我们看看combineReducers的源码,这里也是去掉了一些错误警告的代码和一些错误处理方法:

export default function combineReducers(reducers) {
  // 取出所有的reducer遍历合并到一个对象中
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    // 判断未匹配的refucer
    // ...

    if (typeof reducers[key] === "function") {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

   // 错误处理的一些逻辑
   // ...

  return function combination(state = {}, action) {

    // 错误处理的一些逻辑
    // ...

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      // 对应的reducer
      const reducer = finalReducers[key]
      // 根据指定的reducer找到对应的state
      const previousStateForKey = state[key]
      // 执行reducer, 返回当前state
      const nextStateForKey = reducer(previousStateForKey, action)
      // nextStateForKey undefined的一些判断
      // ...

      // 整合每一个reducer对应的state
      nextState[key] = nextStateForKey
      // 判断新的state是不是同一引用, 以检验reducer是不是纯函数
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

其实到这里可以简单的看出combineReducers就是把多个reducer拉伸展开到到一个对象里,同样也把每一个reducer里的state拉伸到一个对象里。

从bindActionCreators谈如何自动dispatch

现有的store每一次state的更新都需要手动的dispatch每一个action,而我们其实更需要的是自动的dispatch所有的action。这里就用到了bindActionCreators方法。

现在我们来看看bindActionCreators的源码

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

export default function bindActionCreators(actionCreators, dispatch) {
  // 返回绑定了this的actionCreator
  if (typeof actionCreators === "function") {
    return bindActionCreator(actionCreators, dispatch)
  }

  // actionCreators类型判断的错误处理
  // ...

  // 为每一个actionCreator绑定this
  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === "function") {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

其实我们在react项目中对这个方法是几乎无感知的,因为是在react-redux的connect中调用了这个方法来实现自动dispatch action的,不然需要手动去dispatch一个个action。

从compose谈函数组合

compose是Redux导出的一个方法,这方法就是利用了函数式的思想对函数进行组合:

// 通过从右到左组合参数函数获得的函数。例如,compose(f, g, h)与do(...args)=> f(g(h(... args)))相同。
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)))
}
从applyMiddleware谈如何自定义dispatch

我们的action会出现同步的场景,当然也会出现异步的场景,在这两种场景下dispacth的执行时机是不同的,在Redux中,可以使用middleware来对dispatch进行改造,下面我们来看看applyMiddleware的实现:

import compose from "./compose"

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))
    // 通过从右到左组合参数函数获得的函数。例如,compose(f, g, h)与do(...args)=> f(g(h(... args)))相同。
    // 对dispatch改造
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
结语

到此,Redux源码的部分就分析完了,但是在具体和React结合的时候还需要用到react-redux,下一篇文章,我将深入到react-redux的源码学习,来探索在react中,我们如何去使用Redux。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105663.html

相关文章

  • 前端最实用书签(持续更新)

    摘要:前言一直混迹社区突然发现自己收藏了不少好文但是管理起来有点混乱所以将前端主流技术做了一个书签整理不求最多最全但求最实用。 前言 一直混迹社区,突然发现自己收藏了不少好文但是管理起来有点混乱; 所以将前端主流技术做了一个书签整理,不求最多最全,但求最实用。 书签源码 书签导入浏览器效果截图showImg(https://segmentfault.com/img/bVbg41b?w=107...

    sshe 评论0 收藏0
  • React 之容器组件和展示组件相分离解密

    摘要:的绑定库包含了容器组件和展示组件相分离的开发思想。明智的做法是只在最顶层组件如路由操作里使用。其余内部组件仅仅是展示性的,所有数据都通过传入。 Redux 的 React 绑定库包含了 容器组件和展示组件相分离 的开发思想。明智的做法是只在最顶层组件(如路由操作)里使用 Redux。其余内部组件仅仅是展示性的,所有数据都通过 props 传入。 那么为什么需要容器组件和展示组件相分离呢...

    QLQ 评论0 收藏0
  • 结合 Google quicklink,react 项目实现页面秒开

    摘要:最后,状态管理与同构实战这本书由我和前端知名技术大佬颜海镜合力打磨,凝结了我们在学习实践框架过程中的积累和心得。 对于前端资讯比较敏感的同学,可能这两天已经听说了 GoogleChromeLabs/quicklink这个项目:它由 Google 公司著名开发者 Addy Osmani 发起,实现了:在空闲时间预获取页面可视区域内的链接,加快后续加载速度。如果你没有听说过 Addy Os...

    warkiz 评论0 收藏0

发表评论

0条评论

remcarpediem

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<