读 Redux 源码笔记

701 阅读7分钟

1. Redux 基础

Redux 三大原则

  1. 单一数据源;(redux 只有一个数据源,)
  2. State 只读; (State 不能被直接修改,只能 dispatch 一个 action 的方式来返回一个新的状态);
  3. 只能通过纯函数(reducer)去修改store;(reducer 描述了如何将 action 里的信息和原 state 组合成一个新的状态)

如何使用

Redux 中文文档Redux 文档已经详细的描述了 Redux 的基础使用、进阶和一些技巧,务必详细阅读

简单来说,首先我们需要创建一个 store,用于存放 State

const configStore => () {
	const store = createStore(reducers, initState, middlewares)
	reutrn store
}

然后创建各个reducer,并使用 combineReducer 来合成它们。

const count = (state={}, acrtion) => {...}
const name = (state={}, acrtion) => {...}

export default combineReducer({
	count,
	name	
})

然后创建需要的 action,通常我们使用 action creater 来创建 actionaction 描述发生了什么,它承载了当前操作的操作类型和数据

const addCount = (count) => ({
	type: "ADD",	// 这个表示当前action的操作类型,在reducer中需要依据action type 来执行相应的操作;
	count					// action 的操作数据
})

然后只要使用 dispatch(addCount(xxx)),就能修改store里的count state了。

但是通常情况下,我们会借助第三方库 react-redux 来帮忙封装,所以不需要直接手动在组件中操作 dispatch,库 react-redux,将这个操作移到了 mapDispatchToProps 中了。

详见 react-redux 文档

本文将总结阅读源码后的感触。

2. ReducercombineReducers

Redux 中,我们可以使用 reducer 来创建新的 state

第一次创建 state 是什么时候?

我们知道即使我们没有给 createStore 传递 initStateRedux 也会生成一个初始 state 树,如下面的例子,redux 会返回 { count: 0, name: { first: 'huanqiang', last: 'wang' } } ,那么这是如何实现的呢?

const countReducer = (state = 0, action) => {
  switch (action.type) {
    case 'ADD':
      return state + 1
    case 'DIV':
      return state - 1
    default:
      return state
  }
}

const firstNameReducer = (state = 'wang', action) => {
  switch (action.type) {
    case 'FIRST_LONG_CHANGE':
      return 'huanqiang'
    case 'FIRST_SHORT_CHANGE':
      return 'hq'
    default:
      return state
  }
}

const lastNameReducer = (state = 'huanqiang', action) => {
  switch (action.type) {
    case 'LAST_LONG_CHANGE':
      return 'wang'
    case 'LAST_SHORT_CHANGE':
      return 'w'
    default:
      return state
  }
}

const nameReducer = combineReducer({
  first: firstNameReducer,
  last: lastNameReducer
})

const reducers = combineReducer({
  count: countReducer,
  name: nameReducer
})

如果你有心的话,当你打印了所有的 action 的时候,你在应用启动后会在 chrome console 窗口看到 @@redux/INITxxxxxxx 字样的信息,这个信息就表示 Redux 进行了 State 的初始化,Redux 在你执行 createStore 的时候,在 return 之前进行了一次 dispatch,而 action 正是 { type: ActionTypes.INIT }

reducers 是什么样子的?

其实这个问题和 combineReducer 会得到一个怎样结构的返回值是一样的。

我们把上面的例子中的 reducers 简化一下其实就是如下函数,接受最顶层的 state,然后返回一棵 state 的树。同时 Redux 处理每一个子 reducer 的时候,都会从树中获取属于该 reducerstate,然后再作为参数传递进去,比如 countReducerRedux 就是先获取属于 count 的状态 state['count'],然后和 action 一起作为参数传入 countReducer,并将其返回值作为 countvalue

function(state={}, action) {
	return {
		count: countReducer(state['count'], action),
		name: (state1={}, action) => {
			first: firstNameReducer(state1['first'], action),
  		last: lastNameReducer(state1['last'], action)
		}(state['name'], action)
	}
}

这里我们放一个简化的 combineReducers 的源码:

这个是我自己实现的,和官网的略有出入。

export default reducers => {
  return (state = {}, action) => {
    // 初始化新的state
    const nextState = {}
    const reducerKeys = Object.keys(reducers)

    let hasChanged = false
    for (const key of reducerKeys) {
      // key 就是我们传入 combineReducers 函数的 object 的 key 值,也就是 state 的 key。比如 上文的 count。
      // reducer 就是我们传入 combineReducers 函数的 object 的 value。
      const reducer = reducers[key]
      // 获取之前的 state
      const prevState = state[key]
      // 执行 reducer 函数
      nextState[key] = reducer(prevState, action)

      if (!hasChanged) {
        hasChanged = prevState !== nextState[key]
      }
    }
    return hasChanged ? nextState : state
  }
}

看吧, combineReducers 函数是如此简单,以至于都没什么好写的,至于dispatch函数,其实更加简单,去掉边界条件判断和监听操作之后,核心 change state 的代码就这么一行。

currentState = currentReducer(currentState, action)

然后是 getState,去掉边界条件判断也超级简单:

function getState() {
  return currentState
}

3. applyMiddleware

首先,先上一个中间件的示例:

来自于 Redux 中文文档 - Middleware 一文 最下方的例子

/**
 * 记录所有被发起的 action 以及产生的新的 state。
 */
const logger = store => next => action => {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd(action.type)
  return result
}

从这个中间件中,我们可以看出 middleware 的函数签名:store => next => action => {} ,其含义分别如下:

  1. store:其实这个不是 createStore 的到的那个 store,它是一个简化版,内容如下:

    const storex = {
    	getState: store.getState,
    	dispatch: (...args) => dispatch(...args)
    }
    
  2. next:其实即使dispatch,之不过它是被各个 middleware 包装过的。

  3. action:这个就是 dispatch 中传入的 action。(每个dispatch 函数都需要 return action ?)

appleMiddleware 函数的实现及执行流程

知道这些后,我们来看一下 appleMiddleware 函数的实现及执行流程。

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

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

第 2-3 行:首先,我们 applyMiddleware 返回了一个接受 createStore 的函数,而且在第三行还使用这个 createStore 创建新一个 store看到这里你可能就要问了,我们不是通过代码 const store = createStore(reducer, initStore, appleMiddleware(…middlewares)) 创建了 store 了吗?这里为什么还要来一次,为什么要在这里操作?只在 createStore 里不行吗?

其实这和中间件的函数签名有关,中间件的函数签名要求在处理中间件的时候必须有 store。但是我们知道 store 只要当 createStore 函数执行完之后才会创建并返回,而 appleMiddleware 明显需要在 createStore return之前执行完并包装好 dispatch,这就陷入了先有鸡还是先有蛋的问题了。

这里 Redux 就进行了非常机智的操作,当判断到有 appleMiddleware(…middlewares) 传入的时候,createStore 就进入一个新的函数(也就是appleMiddleware的返回值),保留前两个参数再重新执行一次createStore(就是我们这里的第2-3行),然后让第一个 createStore 带着该新函数的返回值 return 掉,不再继续执行。

// createStore 函数
if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error('Expected the enhancer to be a function.')
  }
	// 这里的 enhancer 就是 appleMiddleware(…middlewares),即 createStore => (...args) => {}
  return enhancer(createStore)(reducer, preloadedState)
}

第 11-15 行:这里创建 中间件的函数签名 中的 store,并且让每一个中间件都接受该参数,并返回新的函数,该函数的签名为 next => action => {}

第 16 行:这里是整个 appleMiddleware 函数的核心,就是把所有的 middlewarestore.dispatch 层层包装起来。首先,我们来看下 compose 函数:

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

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 说的很清楚了,就是接受一串函数,然后包起来后返回,比如现在给 compose 传入 f、g、h 这三个函数,那么返回的新函数就是 (...args) => f(g(h(...args)))

其实这是比较好理解的,当传入函数 f、g、h 后,这个函数就相当于:

[f, g, h].reduce((a,b) => (...args) => a(b(...args)))

// 在reduce第一个执行之后,函数 f、g 都被传入 reduce 进行了执行,所以应该得到如下结果:
// 我们知道 f、g 这两个函数的函数签名显示了这两者都需要接受两次参数才能被执行,而在这里,无论是f函数还是g函数都只接受了一次传参,所以不会被执行。(偏函数的性质)????
// 所以说我们现在得到了一个新的函数 first,而这个函数只要接受一次参数即可执行。??????
const first = (...args) => f(g(...args))

// 进行进行第二次执行的时候,a 就是 first,b 就是 h,所以过程如下:
const second = (...args) => first(h(...args))
// 也就是说 first 函数现在有了实参: h(...args),所以first函数被执行,结果如下:
const result = (...args) => f(g(h(...args)))

所以在执行完 compose(…chain) 之后,我们得到了如下函数:

(...args) => middleware1(middleware2(...middlewarenX(...args)..))

然后传入 store.dispatch,得到一个签名为 action => {} 的新函数,这就是我们的 dispatch 函数了。

最后把 store 中没有改变后信息和新的 dispatch 组合成新的 store并返回即可。

不用 appleMiddleware,如何包装 dispatch

首先,我们先来看一下如果不用 appleMiddleware,我们如何简单使用 middleware 包装 dispatch

function addLoggingToDispatch(store) {
	const oldDispatch = store.dispatch
  return action => {
  	console.group(action.type)
    console.info('dispatching', action)
    let result = oldDispatch(action)
    console.log('next state', store.getState())
    console.groupEnd(action.type)
    return result
  }
}

store.dispatch = addLoggingToDispatch(store)

这里推荐 javascript-redux-wrapping-dispatch-to-log-actions 的12-17课时。