带着问题看redux源码

前言

作为前端状态管理器,这个比较跨时代的工具库redux有很多实现和思想值得我们思考。在深入源码之前,我们可以相关注下一些常见问题,这样带着问题去看实现,也能更加清晰的了解。

常见问题

大概看了下主要有这么几个:

  1. redux三大原则
    这个可以直接参考官方文档
  2. redux 的优缺点。 关于优缺点,太主观了大家见仁见智。
  3. redux中间件相关,洋葱模型是什么,常见中间件。

背景

有关acton,reducer相关的部分可以看我前面的文章。我们主要关注针对store和中间件相关的部分来解读。

store的创建

作为维护和管理数据的部分,store在redux中的作用十分重要。在action发出指令,reduxer进行数据更新之后,监听数据变化和同步数据更新的操作都要借助store来实现。

createStore 输入和输出

首先看下createStore的使用,即常见的就是接受经过combineReducers处理之后的reducer和初始的state

import reducer from './reducers'
const store = createStore(reducer,initialState)

此外还可以接受第三个参数enhancer(增强器,一般就是applyMiddleware)


/**
 * 创建管理state 树的Redux store
 * 应用中只能存在一个store,为了区分不同action对应的reducer,
 * 可以通过combineReducers来关联不同的reducer
 * @param {Function} reducer   combineReducers关联之后的reducer
 * @param {Object} preloadedState 初始state
 * @param {Function} enhancer 可以增强store功能的函数,例如中间件等。唯一适合
 * @returns 返回一个Store 以维护state和监听变化
 */
export default function createStore(reducer, preloadedState, enhancer) {
  // 如果第二个参数为func,redux认为忽略了初始state,而是
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // enhancer增强剂,即利用中间件等来增强redux能力
    enhancer = preloadedState
    preloadedState = undefined
  }
  // 返回具有dispatch等属性的对象 即store
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
 } 

按照一般的执行顺序,我们先看下对于参数的处理(平时大家也是一样,一个函数,执行之前尽量判断入参是否符合预期,避免直接处理造成的错误)

入参处理

对于三个参数,后两个是非必填的,但如果第二个参数是function,reduxe认为其实encher,不然初始状态是个函数不符合redux的预期,只能这样处理了。

// 如果第二个参数为func,redux认为忽略了初始state,而是
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // enhancer增强剂,即利用中间件等来增强redux能力
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 对于存在的enhancer,高阶函数函数的用法,
    // 接受createStore返回一个增加功能之后的函数,然后再传入相关reducer得到store。
    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  // 一切符合预期,没有 enhancer,那么初始赋值
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  // 监听队列
  let nextListeners = currentListeners
  // dispatch标识
  let isDispatching = false

  // 初始状态更新之后,声明init状态完成。
  dispatch({ type: ActionTypes.INIT })

dispatch的实现

dispatch的作用就是根据action,执行对应reducer以更新state。并执行监听队列。
下面就来看dispatch的用法和实现。
常见使用:

// redux要求 参数必须为纯对象
dispatch({ type: ActionTypes.INIT })

那么对于纯对象,redux做了些什么呢

 /**
   * 通知方法,参数为纯的js对象,标明更新内容
   * @param {Object} action
   */
  function dispatch(action) {
    // 是否满足纯净对象
    if (!isPlainObject(action)) {
      throw new Error(
        '省略'
      )
    }
    // 必须的type是否存在
    if (typeof action.type === 'undefined') {
      throw new Error(
        '省略'
      )
    }
    // 判断是否处于某个action的dispatch中,大家一起dispatch可能死循环
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // 开始dispatch,加锁,标明状态
      isDispatching = true
      // 将当前状态和更新action,传给当前reducer处理
      // 这里可以回想下我们reducer中的两个参数,state和action 对应的是什么
      /**
       * const todos = (state = [], action) => {
       */
      currentState = currentReducer(currentState, action)
    } finally {
      // 有异常,锁置为false,不影响别的dispatch
      isDispatching = false
    }
    // 执行dispatch,并且更新当前监听队列为 最新队列
    const listeners = (currentListeners = nextListeners)
    // 依次执行,监听器
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

createStore初始化完成之后会执行dispatch({ type: ActionTypes.INIT }),此时执行初始化操作。

我们要关注下currentState的计算,
将currentState,action传给reducer处理,然后更新currentState。

针对初始化来说currentState其实就是initState:

// 初始化状态
let currentState = preloadedState
/****省略***/
// 这里可以回想下我们reducer中的两个参数,state和action对应的值
currentState = currentReducer(currentState, action)

reducer示例:

const todos = (state = [], action) => {
switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    }

getSate实现

getState就是获得store的state。这个比较简单。当结合react-redux使用时,其会帮我们进行操作。我们就不用自行调用这个方法了,所以不要疑惑从哪里获取的state。

/**
   * 返回应用当前状态
   * 不过需要看下当前是否有更新正在进行,是的话则提示
   */
  function getState() {
    // 判断是否isDispatching 中
    if (isDispatching) {
      throw new Error('省略')
    }
    return currentState
  }

subscribe

subscribe是比较重要的一个方法,用来供我们监听状态的变化,以执行相关操作。
例如react-redux中的handleChange 会对是否pure组件及state进行对比,以提升渲染效率。

示例:

 this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))

实现:
返回的是一个函数,可以进行unsubscribe操作。

/**
   * 订阅通知
   */
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        '省略'
      )
    }
    // 是否已经监听过
    let isSubscribed = true
    // 监听队列是否相同,区分开,操作nextListeners
    ensureCanMutateNextListeners()
    // 新增监听事件
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          '省略'
        )
      }
      // 注册完成,可以进行取消操作
      isSubscribed = false
      // 保持事件队列 同步
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 删除监听事件
      nextListeners.splice(index, 1)
    }
  }

replaceReducer

这个开发比较少用,用于热更新

// 用于reducer的热替换,开发中一般不会直接使用
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    // 更新值之后,进行dispatch。
    dispatch({ type: ActionTypes.REPLACE })
  }

到这里createStore已经解析完成了,大家应该了解该方法到底做了些什么操作吧。
简单概括一下就是:接收reducer和initState,返回一个store 对象。该对象提供了监听、分发等功能,以实现数据的更新。

实际使用中的问题

经过上面的解读之后,对于redux的常规应用应该有所了解了。不过实际使用中可能会遇到些问题。
例如action要求是纯对象,而我们获取数据一般是异步的,这就需要借助redux-thunk这个中间件了。
actionCreater返回一个函数。如下:

export function func1() {
  return dispatch => {
      dispatch({
      type:'test',
      data:'a'
      })
  }
}

在了解如何实现之前,需要先看下redux中间件的原理。
因为reducer更多的关注的是数据的操作,对于一些公共的方法,需要抽离出来,不过这些方法在何时使用呢,redux为我们提供了中间件来满足需求。

redux中间件原理

redux 借鉴了 Koa里 middleware 的思想,即鼎鼎大名的洋葱模型。

不过这里请求对应的是dispatch的过程。

每次dispatch的过程中,都要依次将中间件执行一遍。
遇到阻塞或者操作完成,执行下个中间件,直到执行完成,以便我们事先日志,监控、异常上报等功能。
那么redux 又是如何支持中间件的呢。这就离不开applyMiddleware了。
这里前面的

applyMiddleware实现思路

实现思想比较简单,通过科里化和compose,为符合规范的中间件分配访问dispatch和store的途径,以便在不同阶段来自定义数据更新。
例如异步操作,返回的不是对象,那么就执行返回的函数,然后调用下一个中间件。等异步请求结束,再次dispatch 对应的action。

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.`
      )
    }
    // 赋予每个中间件访问store的能力。
    const middlewareAPI = {
      getState: store.getState,
      // 箭头函数保存dispatch,保证其的同步更新
      dispatch: (...args) => dispatch(...args)
    }
    // 串联中间件,并赋予每个中间件访问dispatch的能力。
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 关联dispatch与中间件,组合调用之后得到类似下面的新对象
    // dispatch = f1(f2(f3(store.dispatch))));
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

这样执行之后返回的,对象就是增强之后的store了。

compose的实现

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

结合redux-thunk示例

redux-thunk源码,实现也很优雅,对于返回的function,将dispatch等参数传递进去,然后执行,等待回调异步完成再dispatch。对于正常对象则进行下一步。

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    // 每次dispatch的时候都会进行判断,如果是个函数,那就执行函数,不再进行下一步吧,这样就避免了,函数不满足action要求的问题
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

那么实际使用时,在createStore时加入该中间件即可:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
const store = createStore(
  reducer,
  applyMiddleware({
  ...middleware,
  thunk})
)

那么到这里对于redux的中间件 也就是问题2,我想大家也比较清楚了。
对于常见中间件可以参考

结束语

参考文章

redux中文文档
深入React技术栈
加上重读redux源码一带着问题看 react-redux 源码实现总算将redux及react-redux重读了一遍。可能有人会说道这些源码,看完也会忘,有这个必要吗。我感觉分情况来看,如果我们只是使用,那么看官方文档就可以了,当遇到某些疑问好像找不到贴切解释的时候,不放一看。
此外也是学习大佬们的设计思路和实现方式,有的放矢才能开卷有益。

原文地址:https://www.cnblogs.com/pqjwyn/p/10900323.html

时间: 2024-10-23 13:21:57

带着问题看redux源码的相关文章

带你逐行阅读redux源码

带你逐行阅读redux源码 redux版本:2019-7-17最新版:v4.0.4 git 地址:https://github.com/reduxjs/redux/tree/v4.0.4 redux目录结构 +-- src // redux的核心内容目录 | +-- utils // redux的核心工具库 | | +-- actionTypes.js // 一些默认的随机actionTypes | | +-- isPlainObject.js // 判断是否是字面变量或者new出来的objec

小编带着小白看springboot源码7

上一节在springboot中配置了servlet三大组件以及嵌入式servlet容器,并且还简单的说了如何切换不同的容器. 这一节就来说说如何改变servlet容器的配置参数,一般有两种常见的方式,第一种:配置文件(properties和yml):第二种:往容器里添加组件的方式 注意注意:我这里说的容器和嵌入式容器不一样的,容器指的是ioc容器,嵌入式servlet容器值得是servlet容器,不要混淆了 1.通过配置文件的方式配置 yml为例,下图所示,这是配置容器启动的端口而且我们可以点开

Chrome自带恐龙小游戏的源码研究(完)

在上一篇<Chrome自带恐龙小游戏的源码研究(七)>中研究了恐龙与障碍物的碰撞检测,这一篇主要研究组成游戏的其它要素. 游戏分数记录 如图所示,分数及最高分记录显示在游戏界面的右上角,每达到100分就会出现闪烁特效,游戏第一次gameover时显示历史最高分.分数记录器由DistanceMeter构造函数实现,以下是它的全部代码: 1 DistanceMeter.dimensions = { 2 WIDTH: 10, //每个字符的宽度 3 HEIGHT: 13, //每个字符的高 4 DE

Chrome自带恐龙小游戏的源码研究(六)

在上一篇<Chrome自带恐龙小游戏的源码研究(五)>中实现了眨眼睛的恐龙,这一篇主要研究恐龙的跳跃. 恐龙的跳跃 游戏通过敲击键盘的Spacebar或者Up来实现恐龙的跳跃.先用一张图来表示整个跳跃的过程: 首先规定向下为正方向,即重力加速度(g)为正,起跳的速度(v)为负,恐龙距离画布上方的距离为yPos: 每一帧动画中,速度都会与重力加速度相加得到新的速度,再用新的速度与yPos相加得到新的yPos,改变恐龙的位置为新的yPos,表现出来为yPos不断减小: 当恐龙升至最高点,此时速度为

Chrome自带恐龙小游戏的源码研究(四)

在上一篇<Chrome自带恐龙小游戏的源码研究(三)>中实现了让游戏昼夜交替,这一篇主要研究如何绘制障碍物. 障碍物有两种:仙人掌和翼龙.仙人掌有大小两种类型,可以同时并列多个:翼龙按高.中.低的随机飞行高度出现,不可并行.仙人掌和地面有着相同的速度向左移动,翼龙则快一些或慢一些,因为添加了随机的速度修正.我们使用一个障碍物列表管理它们,当它们移出屏幕外时则将其从列表中移除.同时再用一个列表记录它们的类型: 1 Obstacle.obstacles = []; //存储障碍物的数组 2 Obs

redux源码解析,函数式编程

提到redux,会想到函数式编程.什么是函数式编程?是一种很奇妙的函数式的编程方法.你会感觉函数式编程这么简单,但是用起来却很方便很神奇. 在<functional javascript>中,作者批评了java那种任何东西都用对象来写程序的方式,提倡了这种函数式编程. 之前看过一些函数式编程的例子(以下简称fp).提到fp会想到underscore和lodash,你会看到lodash的包中,唯一一个文件夹就是fp,里面是fp相关的函数. 在redux中,也是运用了很多fp的函数.其实在写js中

Chrome自带恐龙小游戏的源码研究(七)

在上一篇<Chrome自带恐龙小游戏的源码研究(六)>中研究了恐龙的跳跃过程,这一篇研究恐龙与障碍物之间的碰撞检测. 碰撞盒子 游戏中采用的是矩形(非旋转矩形)碰撞.这类碰撞优点是计算比较简单,缺点是对不规则物体的检测不够精确.如果不做更为精细的处理,结果会像下图: 如图所示,两个盒子虽然有重叠部分,但实际情况是恐龙和仙人掌之间并未发生碰撞.为了解决这个问题,需要建立多个碰撞盒子: 不过这样还是有问题,观察图片,恐龙和仙人掌都有四个碰撞盒子,如果每次Game Loop里都对这些盒子进行碰撞检测

Redux源码分析之compose

Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分析之compose      解读之前先了准备一下基本知识 rest参数  形式为...变量名,用于获取函数的多余参数 ,该变量将多余的参数放入数组中, 只能是参数的最后一个. 扩展运算符 扩展运算符(spread)是三个点(...).它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序

Chrome自带恐龙小游戏的源码研究(五)

在上一篇<Chrome自带恐龙小游戏的源码研究(四)>中实现了障碍物的绘制及移动,从这一篇开始主要研究恐龙的绘制及一系列键盘动作的实现. 会眨眼睛的恐龙 在游戏开始前的待机界面,如果仔细观察会发现恐龙会时不时地眨眼睛.这是通过交替绘制这两个图像实现的: 可以通过一张图片来了解这个过程: 为实现图片的切换,需要一个计时器timer,并且需要知道两张图片切换的时间间隔msPerFrame.当计时器timer的时间大于切换的时间间隔msPerFrame时,将图片切换到下一张,到达最后一张时又从第一张