blog icon indicating copy to clipboard operation
blog copied to clipboard

redux源码分析与设计思路剖析

Open WisestCoder opened this issue 8 years ago • 1 comments

1、前言

redux是近期前端开发中最火的框架之一。它是 facebook 提出的 flux 架构的一种优秀实现;而且不局限于为 react 提供数据状态处理。它是零依赖的,可以配合其他任何框架或者类库一起使用。。然而,很多人不清楚它是什么,它有什么好处。

正如官方文档中描述的,redux对于JavaScript应用而言是一个可预测状态的容器。换句话说,它是一个应用数据流框架,而不是传统的像underscore.js或者AngularJs那样的库或者框架。

redux最主要是用作应用状态的管理和数据流的处理。redux用一个单独的常量状态树(对象)保存这一整个应用的状态(官方推荐只有一个state),这个对象不能直接被改变。随着一个项目不停地迭代,项目会越来越庞大,整个应用的数据变得越来越不可控,Redux能完成对数据流的控制,将所有的数据变化变得可预测、可控。

redux的源码非常的简洁,总共加起来就几百行,所以不难理解;建议先去熟悉redux的API和用法再来看本文,会更得心应手。

2、源码结构

src
├── utils                #工具函数
├── applyMiddleware.js
├── bindActionCreators.js        
├── combineReducers.js     
├── compose.js       
├── createStore.js  
└── index.js             #入口 js

2.1 index.js

redux的源码非常简单,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'

function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    '。。。'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}

这里的 isCrushed 函数主要是为了验证在非生产环境下 redux 是否被压缩(默认情况下,isCrushed.name等于isCrushed,如果被压缩了,函数的名称会变短,一般会压缩成数字,那么 (isCrushed.name !== 'isCrushed') 就是 true),如果被压缩,就给开发者一个 warn 提示)。

然后就是暴露 createStore combineReducers bindActionCreators applyMiddleware compose 这几个接口给开发者使用,我们来逐一解析这几个 API。

2.2 createStore.js

createStore是redux非常重要的一个API,createStore会生成一个store,用来维护一个全局的state。

import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

// 私有 action
export var ActionTypes = {
  INIT: '@@redux/INIT'
}

export default function createStore(reducer, preloadedState, enhancer) {
  // 判断接受的参数个数,来指定 reducer 、 preloadedState 和 enhancer
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  // 如果 enhancer 存在并且适合合法的函数,那么调用 enhancer,并且终止当前函数执行
  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.')
  }

  // 储存当前的 currentReducer
  var currentReducer = reducer
  // 储存当前的状态
  var currentState = preloadedState
  // 储存当前的监听函数列表
  var currentListeners = []
  // 储存下一个监听函数列表
  var nextListeners = currentListeners
  var isDispatching = false

  // 这个函数可以根据当前监听函数的列表生成新的下一个监听函数列表引用
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  ... getState ...

  ... subscribe ...

  ... dispatch ...

  ... replaceReducer ...

  ... observable ...

  dispatch({ type: ActionTypes.INIT })

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

createStore接受3个参数:reducer, preloadedState, enhancer;第一个参数reducer和第三个参数enhancer我们接下来会具体介绍,第二个参数是preloadedState,它是state的初始值。

createStore的返回值是dispatchsubscribegetStatereplaceReducer[$$observable]: observable,共同组成了一个store,接下来我们也会讲到讲这些方法。

1. action action代表的是用户的操作。redux规定action一定要包含一个type属性,且type属性也要唯一,相同的type,redux视为同一种操作,因为处理action的函数reducer只判断action中的type属性。

2. reducer reducer 接受两个参数,state以及action函数返回的action对象,并返回最新的state,如下reducer的demo

export default (state, action) => {
    switch (action.type) {
        case A:
        return handleA(state)
        case B:
        return handleB(state)
        case C:
        return handleC(state)
        default:
        return state  // 如果没有匹配上就直接返回原 state
    }
}

reducer 只是一个模式匹配的东西,真正处理数据的函数,一般是额外在别的地方写的(当然直接写在reducer中也没问题,只是不利于后期维护),在 reducer 中调用罢了。 reducer 为什么叫 reducer 呢?因为 action 对象各种各样,每种对应某个 case ,但最后都汇总到 state 对象中,从多到一,这是一个减少( reduce )的过程,所以完成这个过程的函数叫 reducer。

3. getState

function getState() {
  return currentState
}

整个项目的currentState 是处于一个闭包之中,所以能一直存在,getState会返回当前最新的state。

4. subscribe

function subscribe(listener) {
  if (typeof listener !== 'function') {
    throw new Error('Expected listener to be a function.')
  }

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

subscribe接收一个listener,它的作用是给store添加监听函数。nextListeners储存了整个监听函数列表。 subscribe的返回值是一个unsubscribe,是一个解绑函数,调用该解绑函数,会将已经添加的监听函数删除,该监听函数处于一个闭包之中,会一直存在,所以在解绑函数中能删除该监听函数。(由此可见redux源码设计的精巧,多处地方巧用闭包,精简了许多代码。)

5. dispatch

function dispatch(action) {
  if (!isPlainObject(action)) {
    throw new Error(
      'Actions must be plain objects. ' +
      'Use custom middleware for async actions.'
    )
  }

  if (typeof action.type === 'undefined') {
    throw new Error(
      'Actions may not have an undefined "type" property. ' +
      'Have you misspelled a constant?'
    )
  }

  if (isDispatching) {
    throw new Error('Reducers may not dispatch actions.')
  }

  try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
  }

  const listeners = currentListeners = nextListeners
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i]
    listener()
  }

  return action
}

dispatch接收一个参数action。代码会先调用createStore传入的参数reducer方法,reducer接受当前state和action,通过判断actionType,来做对应的操作,并返回最新的currentState。dispatch还会触发整个监听函数列表,所以最后整个监听函数列表都会按顺序执行一遍。dispatch返回值就是传入的action。

6. replaceReducer

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

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

replaceReducer是替换当前的reducer的函数,replaceReducer接受一个新的reducer,替换完成之后,会执行 dispatch({ type: ActionTypes.INIT }) ,用来初始化store的状态。官方举出了三种replaceReducer的使用场景,分别是:

  1. 当你的程序要进行代码分割的时候
  2. 当你要动态的加载不同的reducer的时候
  3. 当你要实现一个实时reloading机制的时候

7. observable 这个API并不是暴露给使用者的,这个是redux内部用的,大家不用深究(千万不要死脑筋啊~~~)。什么,你不信?好吧,实话告诉你,就是内部用的,在测试代码中会用到,感兴趣的可以去test目录下查看(链接)。

2.3 combineReducers.js

// 以下只留下了核心代码(代码太多,全晒的话这篇文章怕是撑不住了~~)
// combination 函数是 combineReducers(reducers) 的返回值,它是真正的 rootReducer
// finalReducers 是 combineReducers(reducers) 的 reducers 对象去掉非函数属性的产物
 // mapValue 把 finalReducers 对象里的函数,映射到相同 key 值的新对象中
function combination(state = defaultState, action) {
    var finalState = mapValues(finalReducers, (reducer, key) => {
      var newState = reducer(state[key], action); //这里调用子 reducer 
      if (typeof newState === 'undefined') {
        throw new Error(getErrorMessage(key, action));
      }
      return newState; //返回新的子 state
    });
    //...省略一些业务无关的代码
    return finalState; //返回新 state
 };

这个函数可以组合一组 reducers,然后返回一个新的reducer。由于redux只维护唯一的state,随着整个项目越来越大,state状态树也会越来越庞大,state的层级也会越来越深,当某个action.type所对应的case 只是要修改state.a.b.c.d.e.f这个属性时,我的 handleCase 函数写起来就非常难看,我必须在这个函数的头部验证 state 对象有没有那个属性。这是让开发者非常头疼的一件事。

combineReducers实现方法很简单,它遍历传入的reducers,返回一个新的reducer,这个新对象的 key 跟传入的reducers一样,它的 value 则是传入的reducers的不同key对应的value展开的{ key: value }。貌似讲的有点绕啊~,举个例子好讲明白:

var reducers = {
    todos: (state, action) { // 此处的 state 参数是全局 state.todos属性
        switch (action.type) {...} // 返回的 new state 更新到全局 state.todos 属性中
    },
    activeFilter: (state, action) { // 拿到 state.activeFilter 作为此处的 state
        switch (action.type) {...} // new state 更新到全局 state.activeFilter 属性中
    }
}
var rootReducer = combineReducers(reducers)

combineReducers内部会将state.todos属性作为todos: (state, action)的state参数传进去,通过switch (action.type)之后返回的new state也会更新到state.todos 属性中;也会将state.activeFilter属性作为activeFilter: (state, action)的state参数传进去,通过switch (action.type)之后返回的new state也会更新到state.activeFilter属性中。

combineReducers是有缺陷的,源码中mapValues只是一级深度的映射,目前redux并没有提供简便的映射到state.a.b一级以上深度的state的方法。这是它目前的不足之处。我们在不改源码的情况下,可以通过嵌套combineReducers来达到目的。

var todosReducers = {
    active: (state, action) => { //拿到全局 state.todos.active
        switch (action.type) {
            case A: //处理 A 场景
            return handleA(state)
            case B: //处理 B 场景
            return handleB(state)
            default:
            return state
        }
    },
    completed: (state, action) => { //拿到全局 state.todos.completed
        switch (action.type) {
            case C: //处理 C 场景
            return handleC(state)
            default:
            return state
        }
    }
}

var todosRootReducer = combineReducers(todosReducers)

var reducers = {
    todos: (state, action) => { //拿到全局 state.todos
        switch (action.type) {
            case A:
            case B:
            case C:
            // A B C 场景都传递给 todosRootReducer
            return todosRootReducer(state, action)
            case D:
            //...handle state
            default:
            return state
        }
    }
}

//rootReducer(state, action) 这里的 state 是真正的全局 state
var rootReducer = combineReducers(reducers)

2.4 bindActionCreators.js

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

export default function bindActionCreators(actionCreators, dispatch) {
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreators的代码比较简单,就是将actionCreator和dispatch联结在一起。对于多个 actionCreator,我们可以像reducers一样,组织成一个 key/action的组合。由于很多情况下,action是 actionCreator 返回的,实际上要这样调用 store.dispatch(actionCreator(...args)),很麻烦是吧?只能再封装一层呗,这就是函数式思想的体现,通过反复组合,将嵌套函数分离。(在这里,我不得不再夸一次redux的作者)

2.5 compose.js

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的代码不难理解,它调用了ES5的Array.prototype.reduce方法,将形如fn(arg1)(arg2)(arg3)...的柯里化函数按照顺序执行。

2.6 applyMiddleware.js

export default function applyMiddleware(...middlewares) {
  return createStore => (reducer, initialState) => {
    var store = createStore(reducer, initialState);
    var dispatch = store.dispatch; //拿到真正的 dispatch
    // 将最重要的两个方法 getState/dispatch 整合出来
    var middlewareAPI = {
      getState: store.getState,
      dispatch: action => dispatch(action)
    };
    // 依次传递给 middleware,让它们有控制权
    var chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain, dispatch); // 再组合出新的 dispatch

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

顾名思义,applyMiddleware就是中间件的意思。applyMiddleware接收中间件为参数,并返回一个以createStore为参数的函数;同时applyMiddleware又是createStore函数中的第三个参数,所以我们回到createStore的代码,找到了:

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

  return enhancer(createStore)(reducer, preloadedState)
}

当createStore中传了第三个参数的时候,会执行enhancer(createStore)(reducer, preloadedState),这是一个柯里化函数;我们可以设想中间件的使用方法:const store = createStore( reducer, applyMiddleware([...中间件]))applyMiddleware([...中间件])的返回值是一个以createStore为参数的函数,这个函数会在createStore中执行,返回的函数也会继续执行,最后返回一个store。我们继续回到applyMiddleware中,在返回store之前,中间件做了什么处理呢?中间件将最重要的两个方法 getState/dispatch整合出来,并传递给中间件使用,中间件处理完之后,返回一个新的dispatch。这里又有疑问,为什么中间件要放在dispatch的时候?借用阮老师的一张图:

image

applyMiddleware把中间件放在一个chain数组中,并通过compose方法(我们上面已经介绍过了),让每个中间件按照顺序一次传入diapatch参数执行,再组合出新的 dispatch。由此可见,每个中间件的格式都应该是接收一个{ dispatch, getState },返回一个(dispatch) => { return function(action) { ... } }

3、工作流程

通过熟悉redux的源码,我们也对redux的代码结构本身有了一个完整的理解,我们可以理解redux的整个工作流程:

  1. 设计全局state的数据结构状态树
  2. 设计更改state数据、状态的actionType常量
  3. 根据actionType,编写actionCreator
  4. 根据各个actionCreator的返回值,用reducer做数据处理
  5. 有个reducer之后,我们用createStore来得到全局唯一的store,来管理state
  6. 用bindActionCreator函数将actionCreator和store.dispatch绑定起来,得到一组能更改state的函数
  7. 分发使用各个状态修改函数(dispatch)

4、结尾

笔者认为redux非常优秀,而且API也非常稳定,同时facebook又推出了适合react项目的react-redux,也说明了redux的强大;redux是前段不可或缺的技术,衷心希望大家能够谈笑风生的学习和使用。

最后向大家安利一款自己用node写的一个react+webpack的脚手架,戳我→

WisestCoder avatar Jul 24 '17 13:07 WisestCoder

写的不错,学习了👍

itboos avatar Oct 28 '18 08:10 itboos