重读redux源码

之前5.1假期的时候,有去读了下redux的源码,当时感触很深,今天重新梳理遍redux的源码:
从源码中的src文件夹看起:


redux源码目录.png

从这里可以看出源码主要分为两大块,一块为自定义的工具库,另一块则是redux的逻辑代码。
上一次我是从createStore.js也就是redux的逻辑代码开始看的,那么这一次我从工具库开始看起:

utils

actionTypes.js

const randomString = () =>
  Math.random()
    // 转化成36进制的字符串
    .toString(36)
    .substring(7)
    .split('')
    .join('.')

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

export default ActionTypes

这个文件返回了两个action的类型,两个类型的值都是通过一个随机数字转化成36进制的随机字符串拼接得到的。

isPlainObject.js

export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

这个方法判断传入参数的原型是否等于Object.prototype换句话说,判断参数的父类是不是Object

warning.js

export default function warning(message) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  try {
    throw new Error(message)
  } catch (e) {}
}

就是打印错误信息,然后为了兼容ie8,做了下判断

逻辑代码

index.js

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

function isCrushed() {}
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

去除警告代码,我们可以看到index.js就是将我们熟悉的一些api导出了,这些api后面我会挨个重新看的哈~

但是__DO_NOT_USE__ActionTypes这个是什么?好像我们日常和文档里都没有见过这个东西呀!回到引用的地方可以看的就是我们之前分析过的actionTypes方法,里面定义了redux自带的action的类型,从这个变量的命名来看,这是帮助开发者检查不要使用redux自带的action的类型,以防出现错误。

createStore.js

接着往下看就是我们最长用的createStore.js文件啦,我们的源码分析也终于进入正餐了~

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

函数createStore接受了三个参数(reducer、preloadedState、enhancer)

reducer就是我们常用的处理数据的纯函数,reducer会根据传入的state和action,返回新的state。

preloadedState就是初始状态

enhancer的意思是增强器,其实就是增强redux功能的函数。

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 当enhancer不为空且为函数时,就执行该函数,并return回去,作为creatStore的返回值。
    return enhancer(createStore)(reducer, preloadedState)
  }

这段代码重点在于enhancer(createStore)(reducer, preloadedState)
可以推出enhancer的结构

function enhancer(createStore) {
  return (reducer,preloadedState) => {
       //逻辑代码
      .......
  }
}

我们举个常见的传入中间件的例子:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

首先看下声明的变量:

  // 初始化数据
  let currentReducer = reducer //把 reducer 赋值给 currentReducer
  let currentState = preloadedState //把 preloadedState 赋值给 currentState
  let currentListeners = [] //当前订阅者列表
  let nextListeners = currentListeners  //新的订阅事件列表
  let isDispatching = false //标记正在进行dispatch

我们了解了具体的参数和变量,接着往下看createStore的对外接口:

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

从dispatch开始看,dispatch接受一个action作为参数

    function dispatch (action) {
      // action要求是一个简单对象,
      //  而一个简单对象就是指通过对象字面量和new Object()创建的对象,如果不是就报错。
      if (!isPlainObject(action)) {
        throw new Error(
          ....
        )
      }
      //  reducer内部是根据action的type属性来switch-case,
      // 决定用什么逻辑来计算state的,所以type属性是必须的。不传会报错
      if (typeof action.type === 'undefined') {
        throw new Error(
          '......'
        )
      }
      // 如果是已经在dispatch的,就报错,避免不一致
      if (isDispatching) {
        throw new Error('.....')
      }
      // 计算新的state,并赋值给currentState
      try {
        isDispatching = true
        // 根据reducer计算出新的state
        currentState = currentReducer(currentState, action)
      } finally {
        isDispatching = false
      }
      // state更新后,将subscribe中注册的监听器,回调触发一遍
      const listeners = (currentListeners = nextListeners)
      for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
      }
      // 返回当前的action
      return action
    }

分析下dispach的操作:

  1. 将isDispatching置为true
  2. 根据reducer计算出新的state
  3. 得到新的state值,isDispatching置回false
  4. 一一通知订阅者state数据已经更新了

getState

function getState () {

    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.'
      )
    }
    //返回当前的state tree
    return currentState
  }

这里就可以看出来getState是直接返回currentState,如果我们直接修改state的话,就不会通知订阅者做更新了。

subscribe

  function subscribe (listener) {
    // listener是state变化时的回调,必须是个函数
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 如果是正在dispatch中,就报错。因为要确保state变化时,
    // 监听器的队列也必须是最新的。所以监听器的注册要在计算新的state之前。
    if (isDispatching) {
      if (isDispatching) {
        throw new Error(
          ......
        )
      }
      //标记有订阅的 listener
      let isSubscribed = true
      //保存一份快照
      ensureCanMutateNextListeners()
      //添加一个订阅函数
      nextListeners.push(listener)
      //返回一个取消订阅的函数
      return function unsubscribe () {
        if (!isSubscribed) {
          return
        }

        if (isDispatching) {
          throw new Error(
            'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
          )
        }

        isSubscribed = false

        ensureCanMutateNextListeners()
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
      }
    }
  1. 首先标记正在订阅
  2. 保存了一份快照
 function ensureCanMutateNextListeners() {
    // 如果nextListeners和currentListeners是否为同一个引用,那么就复制一份,做一个浅拷贝
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

其实我们曾经在dispatch中做了const listeners = (currentListeners = nextListeners)的操作,那为什么这里又要去给他做浅拷贝呢?
因为有这么一种的情况存在。当redux在通知所有订阅者的时候,此时又有一个新的订阅者加进来了。如果只用currentListeners的话,当新的订阅者插进来的时候,就会打乱原有的顺序,从而引发一些严重的问题。

  1. 将新的订阅者加入nextListeners中
  2. 返回一个取消订阅的函数

replaceReducer

 function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

这个方法将reducer替换成传入的参数,并且触发一次state更新操作。

combineReducers.js

因为createStore只接受一个reducer,这个方法主要用来整合reducer。
第一步:检验用户传入的reducers的准确性

// 把reducers对象中的属性转换成一个数组
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
  const key = reducerKeys[i]

  if (process.env.NODE_ENV !== 'production') {
    if (typeof reducers[key] === 'undefined') {
      warning(`No reducer provided for key "${key}"`)
    }
  }

  if (typeof reducers[key] === 'function') {
    finalReducers[key] = reducers[key]
  }
}
//把 finalReducers 对象中可枚举的属性转换成一个数组
const finalReducerKeys = Object.keys(finalReducers)

对传入的reducer做了过滤操作,过滤掉不是函数的部分,然后用定义的finalReducers和finalReducerKeys分别存放拷贝后的reducers和其属性。

第二步:调用assertReducerShape方法

function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
  const reducer = reducers[key]
  const initialState = reducer(undefined, { type: ActionTypes.INIT })

  if (typeof initialState === 'undefined') {
    throw new Error(
      `....`
    )
  }

  if (
    typeof reducer(undefined, {
      type: ActionTypes.PROBE_UNKNOWN_ACTION()
    }) === 'undefined'
  ) {
    throw new Error(
      `......`
    )
  }
})
}

主要用来判断每个reducer是否都有默认返回值

第三步:返回了一个函数,这个函数接收 state 和action
也就是之前creteStore中调用的reducer

currentReducer(currentState, action)
return function combination(state = {}, action) {
    
    ......
    // 定义了一个hasChanged变量用来表示state是否发生变化
    let hasChanged = false
    const nextState = {}
     //循环遍历 finalReducerKeys ,执行所有的 reducer, 所以一定不要有相同的 action.type ,否则状态一定会混乱的
    for (let i = 0; i < finalReducerKeys.length; i++) {
      //获取当前的 key
      const key = finalReducerKeys[i]
      //获取当前 reducer
      const reducer = finalReducers[key]
      //获取当前 key 的 state
      const previousStateForKey = state[key]
      //执行 reducer ,获取 state
      const nextStateForKey = reducer(previousStateForKey, action)
      //判断执行完Reducer后, 返回回来的 nextStateForKey 是 undefined
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      //赋值给 nextState
      nextState[key] = nextStateForKey
      //判断 state 是否经过 Reducer 改变了(判断地址是否改变)
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 改变了的话,返回新的state否则返回之前的state
    return hasChanged ? nextState : state
  }

从这里
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
我们可以看出来,reducer为什么一定要是个纯函数。

compose

当需要使用多个redux中间件 依次执行的时候,需要用到它。

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

这段代码主要重点在reduce,reduce接收一个回调函数,那我们把这段代码中的箭头函数先去掉:

function compose(...funcs) {
  return funcs.reduce(function (total, currentVal) {
      return function () {
          return a(b.apply(undefined, arguments));
      }
  })
}

也就是说如果传入a,b两个参数

function compose(a, b) {
  return a(b())
  }

所以compose的执行顺序是从右向左。

applyMiddleware(...middlewares)

Middleware 主要是包装storedispatch方法。可以同时传入多个middleWare组合到一起使用,形成 middleware链。每个middleware接受StoredispatchgetState 函数作为命名参数,并返回一个函数。

这是文档中的Middleware的解释,之前在讲createStore的时候,讲到了enhancer的结构

function enhancer(createStore) {
  return (reducer,preloadedState) => {
       //逻辑代码
      .......
  }
}

enhancer 实际上就是applyMiddleware(...middlewares)的返回值。
我们继续看applyMiddleware的源码:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args) // 原始的store 此时的dispatch 就是原始的dispatch
    // 声明一个dispatch
    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))
    // 在中间件中调用dispatch的时候, 其实就是调用了这个dispatch, 而不是一开始声明的逻辑
    // 这个dispatch是已经经过compose的包装的函数
    // const composeRes = compose(...chain);
    // 第二行通过传入store.dispatch,
    // 这个store.dispatch就是最后一个 next => action => {}的next参数
    // 所以中间件其实就是改装dispatch
    // dispatch作为有中间件的store的dispatch属性输出
    //  当用户调用dispatch时, 中间件就会一个一个
    // 执行完逻辑后, 将执行权给下一个, 直到原始的store.dispacth, 最后计算出新的state
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

第一步:通过传入的createStore创建了一个store

第二步:声明了一个原始的dispatch,如果在中间件的调用过程中出现了错误,则抛出错误

第三步:定义middlewareAPI,有两个方法,一个是getState,另一个是dispatch,将其作为每个中间件调用的store的参数,整合出一个chain

第四步:通过compose的包装chain,并赋值给dispatch

第五步:将新的dispatch替换原先的store.dispatch

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容