一篇带有“思考”的redux源码阅读

此次阅读的redux的版本是4.x版本,为啥不是最新的呢?由于最新的redux使用typescript来重写了,变化不是特别大,而typescript会有不少的函数类型定义;另外一方面,对js的熟练度确定比ts要好,理解起来也会相对容易一点,还有此次阅读源码的目的是为了了解整个redux的工做流程。redux的源码很是精简,很适合源码的阅读与学习。在阅读以前,对redux有必定的使用会带来更好的效果,一边看一边反思日常所写的内容。下面就开始吧:react

目录

下面的内容是项目的src目录结构git

# src
-- utils/
  -- actionTypes.js
  -- isPlainObject.js
  -- warning.js
-- applyMiddleware.js
-- bindActionCreators.js
-- combineReducers.js
-- compose.js
-- createStore.js
-- index.js
复制代码

项目的文件很是的少,主要逻辑也是在src直接目录下的文件,先作个热身,对简单的utils文件夹入手,这是一些通用的工具方法:github

utils

在看源码的过程当中,对一些工具方法的使用效果保持必定的记忆,对流程的理解上挺有帮助typescript

actionTypes.js

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

actionTypes.js主要定义一些redux内部使用的actionrandomString函数是生成一些随机字符串,保证内部使用的action不会冲突redux

isPlaginObject.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的对象,例如直接声明的对象:const obj = {};若是const objSub = Object.create(obj),那么objSub就不是这里说的“纯对象”。数组

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) {}
}
复制代码

warning.js逻辑比较简单:先把错误的详细信息打印出来,再抛出错误。缓存

核心

热身完以后,咱们来看一下redux的核心,入口在:src/index.js:bash

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
复制代码

这个是index.js的暴露对象,都是从外部引入;除此以外,还有一个叫空函数isCrushed闭包

function isCrushed() {}
复制代码

这个空函数的做用是啥?由于在代码压缩的时候,会对该函数进行重命名,变成function a(){},这样子的函数;这个函数的做用就是,判断若是redux代码被压缩了,并且redux不是运行在production环境,就会报错,提示使用开发版本。app

redux 的核心是createStore,这个核心咱们先放一下,后面再处理,先了解一些辅助该核心的方法:

bindActionCreators

这个方法出场率有时候不是很高,那么它的做用是啥?

首先咱们知道一个词汇actionCreator,这个actionCreator就如命名那样,是用于建立action类型的函数。那bindActionCreators的目的又是什么?这里可能要结合react-reduxconnect方法与“容器组件”、“展现组件”(容器组件 vs 展现组件)来讲明会更好。

一般状况下,若是要让当前组件是用redux,咱们会使用react-reduxconnect方法,把咱们的组件经过connect包裹为一个高级组件,而包裹的过程拿到dispatch与咱们指定的store数据:

// Container.jsx
import { connect } from 'react-redux'

class Container extends React.Component {
    // ...
    render () {
        return <SimpleComponent /> } } export default connect(state => ({ todo: state.todo }))(Container) 复制代码

而这个Container组件咱们能够称之为“容器组件”,由于里面包含了一些复杂的处理逻辑,例如与redux的链接;而若是SimpleComponent组件也有一些操做,这些操做须要更改到redux的内容,这样子的话,处理方法有两个:

  1. SimpleComponent也使用connect处理为高级组件
  2. Containerreduxdispatch方法显示传递到SimpleComponent

这两个方法都不是很好,第一方法会让组件更加复杂,可能与咱们的容器组件-展现组件的姿式有点不一样;第二种方法也能够,可是会让组件变得耦合度高。那能不能让SimpleComponent组件给Container的反馈也经过日常props-event的形式来处理呢,让SimpleComponent感知不到redux的存在?

这个时候就可使用bindActionCreators了;例若有一个action为:{ type: 'increment', value: 1},一般若是Container组件触发能够经过:

// actions.js
const Add = (value) => { type: 'increment', value }
复制代码
// Container.jsx
import Add from './actions.js'

// Container.jsx 某个事件触发触发更新
class Container extends React.Component {
    onClick() {
        dispatch(Add(1))
     }
}
复制代码

利用bindActionCreators处理后,给到SimpleComponent使用则能够这样:

// Container.jsx
import Add from './actions.js'

class Container extends React.Component {
    render () {
        const { dispatch } = this.props // 经过 react-redux 的 connect 方法组件能够获取到
        const actions = bindActionCreator({
            add: Add
        }, dispatch)
        
        return <SimpleComponent {...actions} /> } } // SimpleComponent.jsx function SimpleComponent({ add }) { return <button onClick={() => add(1)}>click</button> } 复制代码

经过bindActionCreators处理后的函数,add,直接调用,就能够触发dispatch来更新,而这个时候SimpleComponent并无感知到有redux,只是当是一个事件函数那样子调用。

image.png

了解到bindActionCreators的做用以后,咱们再来看一下源码就很好理解了:

function bindActionCreator(actionCreator, dispatch) {
  // 使用闭包来屏蔽 dispatch 与 actionCreator
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
export default function bindActionCreators(actionCreators, dispatch) {
  // 当 actionCreators 只有一个的时候,直接返回该函数的打包结果
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // 省略参数类型判断
  // ...
  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    // 只对 { key: actionCreator } 中的函数处理;actionCreators 中的其余数据类型被忽略
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
复制代码

combineReducers

接下来讲一下combineReducers,这个方法理解起来比较简单,就是把多个reducer合并到一块儿,由于在开发过程当中,大多数的数据不会只有一个reducer这么简单,须要多个联合起来,组成复杂的数据。

// () => {} 为每一个对应的reducer
const state = {
    count: () => {},
    userData: () => {},
    oeherData: () => {}
}
复制代码

一般使用compineReducer可让咱们规避一些问题,例如对reducer的传入参数的判断等,保证reduce流程的运转,简化核心代码以下,去掉一部分开发代码,可是会注释做用:

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 检查全部reducer的key值是否合法,reducer是一个函数,则加入到finalReducers
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    // 这里有个判断,若是reducers对象的某个key值的value为undefined,则报错

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

  // 这里有个判断(assertReducerShape),判断是否有 reducer 返回是 undefined
  // 若是有,则先保留这个错误,咱们定义为错误 A
    
  // 这个 combination 函数,每次dispatch都会执行一次
  return function combination(state = {}, action) {
    // 这里有个判断,若是错误A存在,则抛出异常

    // 这里有个判断(getUnexpectedStateShapeWarningMessage),会对数据进行多重判断,
    // 判断有错,则抛出异常,判断的规则有:
    // 1. reducers的数量是否为0
    // 2. 对每次执行reducer传入的state(state的来源后面讲到)是不是“纯对象”(上面有提到)
    // 3. 对每次执行reducer传入的state对象判断,是否该对象全部的字段都是“本身的”(hasOwnProperty),
    // 也就是没有一些从父对象继承,toString ?
    // 第三点其实有点不太了解,由于第二步纯对象已通过滤了?
    
    // 下面这个就是 combineReducers 的核心代码
    let hasChanged = false
    const nextState = {}
    
    // 遍历全部的函数reducer,获取返回值,经过判断先后值的不一样,判断是否发生了变化,有变化,则返回新的state
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      
      // 不容许 reducer 返回的值为 undefined,不然报错
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 这里判断是否改变,是经过判断 reducer 返回的值与以前的值是否一致
      // 因此就突出了“不可变对象”的重要性
      // 若是reducer每次返回的对象是在旧对象上面更改数据
      // 而对象地址没改变,那么 redux 就认为,此次改变是无效的
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}
复制代码

createStore

讲完上面两个辅助方法以后,来说一下建立store的核心createStore的方法;由于createStore方法比较长,下面先看一下概览:

export default function createStore(reducer, preloadState, enhancer) {
    // 判断是否传入各类参数是否符合要求
    // 对于加强器(enhancer)的调用会提早返回
    // 建立store的过程被延后到加强器中
    // ...
    
    // 当前最新的 reducer 与 state
    // listeners 是经过 store实例subscribe的函数数组
    let currentReducer = reducer
    let currentState = preloadedState
    let currentListeners = []
    let nextListeners = currentListeners
    // 当前的reducer是否在执行当中
    let isDispatching = false
    
    // 用于防止 listeners 数组出错,后面讲到
    function ensureCanMutateNextListeners() {}
    // 返回当前最新的 state,给外部函数调用获取内部数据
    function getState() {
        return currentState
    }
    
    // store.subscribe的方法,用于添加 listener,后面有详细讲解
    // 监听 state 的变化
    function subscribe(listener) {}
    
    // 触发 reducer 执行,返回新的 state
    function dispatch(action) {}
    
    // 使用新的 reducer 替换当前的 currentReducer
    // 一般在两种状况下使用:
    // 1. 部分 reducer 异步加载,加载完毕后添加
    // 2. 用于开发时候用,热更新
    function replaceReducer(nextReducer) {
        currentReducer = nextReducer
    }
    
    // TODO 这个了解很少
    function observable () {}
    
    // 触发第一个更新。拿到第一次初始化的 state
    dispatch({ type: ActionTypes.INIT })
}
复制代码

在没有enhancer处理的过程,createStore的过程,都是一些声明的函数与变量,惟一开始执行的是dispatch,如今就从这个dispatch开始讲解:

function dispatch (action) {
    // 判断 action 是不是“纯”函数
    // 判断 action.type 是否存在
    // ...
    
    // 判断当前的dispatch是否在执行中,屡次触发,则报错
    if (isDispatching) { throw new Error() }
    
    try {
      isDispatching = true
      // 尝试去执行 reducer,把返回的 state 做为最新的 state
      // 若是 咱们的 reducer 是使用 combineReducers 方法包裹的话
      // 这里的 currentReducer 为 comineReducer的combination方法
      // 这里回答了以前所说的 combination 方法拿到的第一个参数 state
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    
    // 更新完 state 以后,就会把监听的函数全都执行一遍
    // 注意这里的 currentListeners 被赋值为 nextListeners
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
}
复制代码

整个 dispatch 就结束了,很简单,就是把全部reducer都执行一遍,返回最新的 reducer;若是使用combineReducer来联合全部的reducer的话,至关于执行combination方法,该方法会把被联合的全部reducer都执行一遍,因此这里能解释说,为何在reducer方法的时候,在switch...case要保留一个default选项,由于有可能执行当前reducer的action,是用于触发其余reducer的;这种状况就把当前reducer对应的state返回便可

function reducer(state, action) {
    switch (action.type) {
    case '':
        // ...
        break
    case '':
        // ...
        break
    default:
        return state
    }
}
复制代码

当state经过reducer更新以后,就会把加入监听的listener逐个执行;循环的listenerscurrentListeners,这里要圆一下以前说的ensureCanMutateNextListeners函数与subscribe的行为,函数代码为:

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice()
    }
}

function subscribe(listener) {
    // 省略部分参数与进程判断
    let isSubscribed = true
    
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

        // 省略部分进程可行性判断

        isSubscribed = false

        ensureCanMutateNextListeners()
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
    }
}
复制代码

咱们看到subscribeunsubscribe的过程,只是一个很简单的数组处理添加与删除listeners的过程,可是这两个过程都有执行ensureCanMutateNextListeners的函数。这个函数的做用是:

保证当次触发listeners的过程不受影响

这句话怎么理解呢?能够看到触发listeners也只是把listeners的函数循环执行一遍。但若是listeners由此至终都只是一个数组,那么若是某个listeners执行的内容,再次添加/删除listener,那么这个循环过程就有可能出现问题:

const { dispatch, subscribe } = createStore(/* ... */)

subscribe(() => {
    log('state change')
    // 在 listeners 添加监听方法
    subscribe(() => {
        
    })
    // 或者 移除以前监听的部分方法
    unsubscribe(/* ... */)
})
复制代码

因此ensureCanMutateNextListenerslisteners区分为两个数组,一个是当前循环的数组,另外一个是下次循环的数组。每次触发dispatch都是最近更新的一次listeners快照。

compose 与 applyMiddleware

了解完核心createStore以后,咱们再了解一下加强核心功能的函数:applyMiddleware,由于applyMiddlewarecompose关联很密切,applyMiddleware的实现依赖compose

compose

compose是一个函数,先看一下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)))
}
复制代码

很是的精简;compose的代码的做用是简化代码:

(...args) => f(g(h(...args)))

// 等同于 
compose(f, g, h)(...args)

// compose使用例子
function foo (str) {
    return str + '_foo'
}
function bar (str) {
    return str + '_bar'
}
function baz (str) {
    return str + '_baz'
}
compose(baz, bar, foo)('base') // "base_foo_bar_baz"
复制代码

compose方法就是把上一个函数执行的结果做为下一个函数执行的参数,执行顺序从后往前,传入参数的最后一个函数先被执行。

applyMiddleware

middleware就是一个中间件的概念,简化以下:

image.png

数据通过每一个中间件的处理,会对数据,或者保留一些数据的痕迹,例如写入日志等

applyMiddleware的用法也是相似:

const store = createStore(rootReducer, {}, applyMiddleware(middleware1, middleware2))
复制代码
// applyMiddleware 源码
export default function applyMiddleware(...middlewares) {
    // createStore方法做为参数传入
    // 至关于延迟一步初始化 store
    return createStore => (...args) => {
        const store = createStore(...args)
        let dispatch = () => {/* ... */}

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        
        // 把传入的 middleware 先执行了一遍
        // 把 getState 与 dispatch 方法传入
        // 让 middleware 可以获取到当前 store 的 state与有触发新的 dispatch 能力
        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        
        // 这个时候的 dispatch 不是原有的 createStore 函数中的方法
        // 而是一个通过 middleware 集成的新方法
        // 而原有的 dispatch 方法做为参数,传入到不一样的middleware
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            // 使用当前的 dispatch 覆盖 createStore 的 dispatch 方法
            dispatch
        }
  }
}
复制代码

redux-thunk是一个对于了解middleware很好的例子,下面参照redux-thunk弄一个自定义的middleware, 源码以下:

function customeMiddleware({ dispatch, getState }) {
    return next => {
        return action => {
            if (typeof action === 'function') {
                return action(dispatch, getState)
            }
            
            return next(action)
        }
    }
}
复制代码

为何会函数嵌套那么多层呢?其实每一层都是有缘由的;第一层:

function customeMiddleware ({dispatch, getState}) {
    // ...
}
复制代码

dispatch是可以触发一个完整流程更改state的方法,getState方法用于获取整个reducer的state数据;这两个方法都是给到middleware须要获取完整state的方法。从上面applyMiddleware的方法能够知道,applyMiddleware执行的时候,就先把middleware函数都执行了一遍,返回chains数组:

const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
复制代码

看到这里,会有一个疑问compose执行的顺序是从后面往前执行,可是咱们定义middleware是从前日后的。

chains数组的方法至关于middlewarenext方法(接收参数为next函数,暂时这样子命名),compose执行的时候,至关于next已经执行,而且返回一个新的函数,这个函数是接收一个叫action的函数(暂命名为action函数);由于每一个middleware的接收next函数执行后都是action函数;next函数的next参数就是上一个函数的返回值。执行到最后,dispatch = compose(...)(store.dispatch),dispatch函数实际上是第一个middlewareaction函数

// chain0 表示 chain 数组中的第一个函数,chain1表示第二个,以此类推
// compose 执行顺序为倒序
const action2 = chain2(store.dispatch) // store.dispatch的值是compose()(store.dispatch)传入的
const action1 = chain1(action2)
const action0 = chain0(action1)
复制代码

action0就是最终返回到dispatch函数;当咱们在组件中执行dispatch()的时候,其实是调用action0函数,action0函数能够经过next调用下一个middleware

// action0
action0(action)
const result = next(action) // 这个 next 的函数为 action1
    // action1
    action1(action)
    const result = next(action) // 这个 next 的函数为action2
        // action2
        action2(action)
        const result = next(action) // 这个 next 的函数为 store.dispatch
复制代码

就这样子层层嵌套,把每一个middleware都执行完,最终去到store.dispatch,最终更改好reducer,返回一个全新的state;而这个state也层层冒泡传到最顶层的middlewaremiddleware执行顺序的疑问由此解开。

小结

redux的源码很少,使用起来也很简单,可是里面运用的知识很多,特别是在middleware的时候,须要很细心的看且有较好的基础,否则看起来仍是有点吃力的。另一些用闭包来缓存变量、保存函数执行状态等,用得很精妙。Get.~

安利:若是你们以为文章不错,能够在github给个star,更多文章,能够看个人 github issues

相关文章
相关标签/搜索