redux源码超详解(附举例)

redux源码详解(一)

本次主要分享一下redux的源码解析,以及我在阅读源码的过程中遇到的一些问题。主要分为两部分,第一部分为实现基本的redux功能,主要包含两个js文件的分析,一个是用来生成store对象,另一个是用来实现reduce函数的拆分与合并。第二部分是实现异步的dispatch,主要是通过中间件函数来加强dispatch实现的。

一.实现基本的redux功能

我们先来讲第一部门,如何实现基本的redux功能。
下图是redux官网一张动图,解释了redux的基本运作原理。
在这里插入图片描述

先用createStore函数生成一个store对象,store是一个全局状态机,存放全局的状态state。添加订阅函数,可以在state对象发生变化的时候通知你。如果想改变其中某一个状态,用dispatch发出一个action,dispatch会自动执行reduce函数,该函数会在原来state的基础上重新计算并返回一个新的state对象,状态改变之后,通知所有的订阅函数,改变页面UI。

redux源码分为几个主要文件:
1.createStore.js文件:基本的redux文件,生成全局状态机
2.combineReducer.js文件:用于reducer的拆分
3.applyMiddleware.js文件:用于异步函数的实现

一.createStore.js文件

接下来,我们先来讲一下createStore的源码,该文件主要包含三个方法:
getState:用来获取store中的状态变量
dispatch:发出一个action,改变store中state的值
subscribe:订阅state更新的事件,状态改变时通知订阅事件
下面是createStore.js的源码

function createStore(reducer, preloadedState, enhancer){

  // 增强dispatch函数 start
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    // 判断 enhancer 不是一个函数
    if (typeof enhancer !== 'function') {
      // 抛出一个异常 (enhancer 必须是一个函数)
      throw new Error('Expected the enhancer to be a function.')
    }
    // 调用 enhancer ,返回一个增强版的 store creator
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 增强dispatch函数 end
  
  let currentState = preloadedState // 当前的state
  let isDispatching = false // 是否正在执行dispatch函数
  let currentListeners = [] //当前的监听函数列表
  let nextListeners = currentListeners // 新生成的监听函数列表

  // 获取store中的变量值
  function getState () {
    if (isDispatching) {
      throw new Error('currentState已经传输给reducer函数,不希望我们通过getState的方法获取state对象')
    }
    return currentState
  }

  /**
    触发reducer函数改变state
    通知所有的订阅函数
   */
  function dispatch (action) {
    try {
      isDispatching = true
      // 执行reduce函数,改变state,返回新的state对象
      currentState = reducer(currentState, action)
    } finally {
      isDispatching = false
    }
    // state发生变化时,通知所有的订阅函数
    let listeners = (currentListeners = nextListeners)
    for(let i = 0;i<listeners.length;i++){
      listeners[i]()
    }
    return action
  }

  /**
    新增订阅函数
    返回取消订阅函数
  */
  function subscribe(listener){
    let isSubscribed = true
    // 保证 nextListeners 的变化不影响当前currentListeners
    ensureCanMutateNextListeners()
    // 添加新的监听函数到nextListeners
    nextListeners.push(listener)
    // 返回该订阅函数的取消订阅函数
    return function unsubscribe(){
      // 防止取消订阅两次
      if (!isSubscribed) {
        return 
      }
      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index,1)
      // TODO 为啥
      currentListeners = null
    }
  }
  // 保证 nextListeners 的变化不影响当前 currentListeners
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 调用reducer初始化state对象
  dispatch({})

  return {
    getState,
    dispatch,
    subscribe
  }
}

程序中主要的方法有三个,注释也记录的比较清晰,这里就不细讲了。
在分析源码以及重写源码的过程中,我遇到了几个问题,希望能给大家带来一点帮助:

问题1:可以不通过dispatch而直接修改store中state的值吗? 为什么不推荐这样做?
问题2:在dispatch的时候 为什么不能执行getState方法
问题3:为什么要用currentListeners和nextListeners(store.subscribe嵌套的情况)
问题4:为什么要在最后执行一次dispatch?将state初始化,使得state在初始值的情况下进行更新

问题一:
这个问题之前在项目中遇到过,印象比较深刻。由于getState方法中返回的是currentState本身,而不是它的拷贝,所以可以直接改变currentState对象。但是redux不推荐这样做,因为这样改变state之后,不会通知订阅函数。dispatch是唯一推荐改变state的方式。

问题二:
currentState已经传输给reducer函数,不希望我们通过getState的方法获取state对象

问题三:
问题可以总结为:dispatch时将nextlisteners赋值给currentListeners,并执行currentListeners中的每一个订阅函数。subscribe时nextListeners用来添加删除listener,并确保nextListeners的增删不会影响currentListeners。
既然currentListeners只用到了一处,并且还等于nextlisteners。为什么不能只用一个?
我发现只用nextListeners不用currentListeners,也可以实现基本的功能。但是当订阅函数相互嵌套时,就会出现不一致的情况。我们来举例说明:

在这里插入图片描述
上面的程序当中,订阅函数里面又嵌套了一个订阅函数,这意味着当遍历currentLIsteners数组执行订阅函数时,又会添加一个新的订阅函数到nextListeners数组中,造成结果输出不一致的情况出现。
在这里插入图片描述

通过上图可以发现,两者是同一个引用时、两者引用不同时,得到的打印结果不一样。主要区别在于dispatch方法执行时(遍历currentLIsteners)添加了新的listeners到nextListeners中,这个时候如果currentLIsteners和nextListeners是同一个引用时,相当于遍历currentLIsteners事件的同时,添加了一个新的事件到currentLIsteners中。
所以,要用两个订阅事件集合。我们不希望在dispatch的时候改变订阅函数集。

问题四:
store创建好之后,立即发出一个初始化action,是为了让reducer返回store的初始化状态,否则,创建store之后,调用getState方法得到的就是undefined。

二 .combineReducers.js方法

reducer函数负责生成state,整个应用只有一个state对象,包含所有的数据,对于一个大型应用来说,state必然比较庞大,会导致reducer函数也十分庞大,所以需要对reducer函数进行拆分。并且几个人开发的项目中,我们需要编写各自的reucer函数并合并在一起,因为createStore只能接收一个reducer函数。这就是combineReducers函数做的事情。下面来举例说明:
假设我们项目当中有两个reducer函数,reducerI 和 reducerII 函数,将两个reducer函数以对象的形式传入combinereducers函数,函数执行返回的finalReducer作为reducer函数传给createStore,实现了原本reducer函数相同的功能。
假设我们项目当中有两个reducer函数,reducerI 和 reducerII 函数,分别负责项目中不同的模块,由于传入到createStore中的reducer函数只有一个,所以我们需要将两个reducer函数合并成一个。
将两个reducer函数传入combinereducers函数,返回一个合成的reducer函数,就可以实现一样的redux功能。
在这里插入图片描述
运行结果:
在这里插入图片描述
发现:combineReducers实现了合并reducer函数的功能,我们好奇的是,combineReducers是如何通过传入的action来改变其对应的state,并将state合并成一个对象输出的。接下来,我们来看combineReducers.js源码:

// 参数reducers是一个对象,key值和state的key值一样,value是一个reducer函数
function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 遍历key值,过滤掉value不是reducer函数的值
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    // 过滤掉value不是reducer函数的值
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // 返回一个合成的reducer函数(接收state和action作为参数)
  return function combination(state = {}, action) {
    
    let hasChanged = false  // 记录state对象是否改变的变量
    const nextState = {}  // 下一个状态
    // 遍历key值
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key] // 当前key值对应的状态
      const nextStateForKey = reducer(previousStateForKey, action) //调用reducer后新生成的状态
      nextState[key] = nextStateForKey //将新生成的状态赋值到新状态的对应key值上
      // 通过对比previousStateForKey 和 nextStateForKey是否相等来表明状态是否改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 如果状态改变就返回新的状态,没有,就返回原来的state对象
    return hasChanged ? nextState : state
  }
}
// combineReducers函数实际上也是一个reducer函数,接收state和action作为参数,返回一个新的state

combineReducers接收reducers对象为入参,返回一个合成的reducer函数,利用闭包,实现了reducer合成的功能。实质是通过遍历的形式,将action传入每一个reducer函数,获取初始值或者改变state,返回新的state对象,与普通的reducer函数很相像。

三. applyMiddleware.js文件

在一个项目当中,如果在两个文件中需要请求相同的数据,以往的做法是分别在两个文件中请求,得到数据之后再分别dispatch进行数据保存。这样请求数据的函数其实写了两遍,redux将异步函数的执行封装在了dispatch方法内部,用redux实现异步dispatch之后,只需要在两个文件当中dispatch调用该异步方法即可。
从一开始的dispatch只能接收对象,到现在的dispatch可以执行异步函数,其实是redux用中间件函数增强了dispatch,让dispatch做了更多的事情。
如何能让reducer函数在异步函数结束后自动执行?这就需要用到中间件(middleware)来增强dispatch函数。
下面,我们来举例说明:
在这里插入图片描述
运行结果:
在这里插入图片描述
上述程序运行以及结果说明,加入中间件thunk之后,我们dispatch一个异步的操作,可以在异步操作执行结束之后再执行reducer函数,达到了我们想要的目标。
下面我们来分析一下applyMiddleware.js的源码,搞明白中间件是怎么实现异步操作的。
为了搞明白applyMiddleware.js,我们需要提前了解compose函数和reduce函数的源码:

1)reduce函数的源码

Array.prototype.myReduce = function (fn, init) {
  //数组的长度
  var len = this.length;
  var pre = init;
  var i = 0;
  //判断是否传入初始值
  if (init == undefined) {
    //没有传入初始值,数组第一位默认为初始值,当前元素索引值变为1。
    pre = this[0];
    i = 1;
  }
  for (i; i < len; i++) {
    //当前函数返回值为下一次的初始值
    pre = fn(pre, this[i], i)
  }
  return pre;
}
var arr = [1, 2, 5, 4, 3];
var add = arr.myReduce(function (preTotal, ele, index) {
  return preTotal + ele;
}, 100)
console.log(add);//115

是我们常见的用reduce函数实现的数组迭代求和的例子,在数组上调用reduce,reduce接收的入参是函数fn和数组迭代初始值100,第一次函数的执行是取初始值100和数组第一项1相加,将函数执行的结果101作为函数的第一个入参,并取数组第二项2作为函数fn的第二个入参,第二次执行函数fn,如此迭代下去,直到返回整个数组和初始值相加的结果115。

在数组上执行reduce函数,实际上是迭代数组的每一项,将结果作为入参返回给下一次执行的函数

2)compose函数的源码

function compose() {
  // 将arguments的每一项拷贝到func中去
  var _len = arguments.length;
  var funcs = [];
  for (var i = 0; i < _len; i++) {
    funcs[i] = arguments[i];
  }

  if (funcs.length === 0) {
    return function (arg) {
      return arg;
    };
  } 

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

  return funcs.reduce(function (a, b) {
    return function () {
      // arguments是返回函数的入参
      return a(b.apply(undefined, arguments));
    };
  });
}

下面,我们举例说明compose函数的作用:

const value = compose(function (value) {
  return value + 1;
}, function (value) {
  return value * 2;
}, function (value) {
  return value - 3;
})(2); //-1

将f1 f2 f3 三个函数依次传入compose函数,compose函数的精髓在于reduce函数,其实就是将上一次函数执行的值作为下一次函数的入参。下图是将参数依次带入reduce函数得到的执行结果。可以发现,传入compose的函数数组是按照从右往左的顺序依次执行的。(洋葱函数)
在这里插入图片描述

3)applyMiddleware.js函数的源码

在熟悉了上面两个函数之后,我们就来啃最后一根最难啃的骨头,即中间件函数。中间件函数是在哪里用的呢,它是作为第三个参数传入createStore函数中的,接下来我们先完善createStore,看看它在传入中间件函数时是怎么处理的。
createStore.js 源码:

function createStore(reducer, preloadedState, enhancer){

  // 增强dispatch函数 start
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    //判断 enhancer 不是一个函数
    if (typeof enhancer !== 'function') {
      //抛出一个异常 (enhancer 必须是一个函数)
      throw new Error('Expected the enhancer to be a function.')
    }
    //调用 enhancer ,返回一个新强化过的 store creator
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 增强dispatch函数 end

  // 确保 nextListeners 与 currentListeners 保持一致,且保证 nextListeners 的变化不影响当前 currentListeners
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  
  let currentState = preloadedState // 当前的state
  let isDispatching = false // 是否正在执行dispatch
  let currentReducer = reducer // 当前的reducer
  let currentListeners = [] //当前的监听函数列表
  let nextListeners = currentListeners // 新生成的监听函数列表

  function getState () {
    // 正在执行 dispatch 中,不能调用 store.getState()
    if (isDispatching) {
      throw new Error('改变数据的同时不能获取数据')
    }
    return currentState
  }

  /**
   * 核心函数:触发 state 改变的唯一方法
   * 当发送 dispatch(aciton) 时,用于创建的 store 的 ‘reducer(纯函数)’ 都会被调用一次。调用的传入的参数是当前的 state 和发送 ‘action’,
   * 调用完成后,所有的 state 监听函数都会触发。
   */
  function dispatch (action) {
    if (isDispatching) {
      throw new Error('不能同时改变数据')
    }
    try {
      isDispatching = true
      // 执行reduce函数,改变state,返回新的state对象
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    // state发生变化时,通知所有的订阅函数
    let listeners = (currentListeners = nextListeners)
    for(let i = 0;i<listeners.length;i++){
      listeners[i]()
    }
    return action
  }

  // 新增一个变更监听函数且返回一个解绑函数,作用于 dispatch 内部执行,每当dispatch(action)后所有监听函数都会被触发一次
  function subscribe(listener){
    if (isDispatching) {
      throw new Error('改变数据的同时不能订阅事件')
    }
    let isSubscribed = true
    ensureCanMutateNextListeners()
    // 添加新的监听函数到nextListeners
    nextListeners.push(listener)
    // 返回该订阅函数的取消订阅函数
    return function unsubscribe(){
      if (!isSubscribed) {
        return 
      }
      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index,1)
    }
  }
  // 初始化state对象
  // dispatch({})

  return {
    getState,
    dispatch,
    subscribe
  }
}

上面是加入enhancer入参之后的程序,其中最主要的一行代码就是:

return enhancer(createStore)(reducer, preloadedState)

现在,我们贴上applyMiddleware.js的代码进行对比,看看中间件到底做了什么?

// enhancer(createStore)(reducer, preloadedState) = applyMiddleware(...middlewares)
function applyMiddleware(...middlewares) {
  // applyMiddleware返回一个enhancer函数
  return  (createStore)=> {
    //enhancer函数返回一个生成store的函数
    return (...args) => {
      //生成基本的store函数
      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)
      }
      //将middlewareAPI 注入到中间件 得到中间件函数第二层函数的数组
      const chain = middlewares.map(middleware => middleware(middlewareAPI))
      dispatch = compose(...chain)(store.dispatch)

      return {
        ...store,
        dispatch
      }
    }
  } 
}

对比之前的异步执行程序:在这里插入图片描述
对比上图中的程序可以发现,applyMiddleware函数返回的是一个enhancer函数,enhancer函数接受一个createStore函数作为入参,enhancer函数执行后返回的函数接收reducer、preloadedState作为入参,该函数执行之后,返回的是一个具有加强版disptach方法的store对象。我们再来看applyMiddleware函数的下半部分:
在这里插入图片描述
上图中,做的事情主要就是生成基本的store对象,利用中间件对dispatch函数进行增强,返回一个新的store对象。那,中间件函数是如何对dispatch函数进行加强的呢?我们用两个中间件函数进行举例:
在这里插入图片描述
thunk函数和logger函数是react-redux封装的两个比较常用的中间件函数,分别用来处理异步操作和打印state,他们都有统一的格式:({dispatch,getState}) =>{return next => action => {}}
applyMiddleware得到的chain数组,就是将dispatch和getState注入到中间件函数得到的第二层函数数组,数组的形式为:next => action => {}
下面的f1 f2函数是将中间件入参传入到中间件函数之后得到的chain数组中的函数
在这里插入图片描述

接下来,到最重要的一步:
在这里插入图片描述

在这里插入图片描述
执行代码过后,将f1 f2函数带入到代码当中,可以得到新的dispatch为:
在这里插入图片描述
可以发现dispatch真的被增强了,不仅可以实现异步,还可以打印dispatch前后的state。
在这里插入图片描述
我们用新的dispatch执行异步函数,程序判断action是一个函数,进入到上半部分,开始执行异步函数,setTimeout执行过后,又dispatch一次,又进入到下半部分,开始打印dispatch之前的state,然后调用store.dispatch(原来的dispatch)更新state,最后打印dispatch之后的state。
最后的执行结果是:
在这里插入图片描述
可以发现dispatch真的被增强了,不仅可以实现异步,还可以打印dispatch前后的state。

到底为止,我们就解析结束了redux的源码,希望能给大家带来一丝帮助。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值