造玩具学原理系列 | redux 源码解析及模拟实现

1,074 阅读11分钟

在造火箭之前,我们先分析下 redux 和 react-redux 源码。

准备工作

  1. redux 原理
  2. react-redux 原理

开始上手

  1. mini 版本的 Redux 实现

亮点学习

  1. 闭包(applyMiddleware)
  2. 设计模式:发布订阅(subscribe、dispatch)
  3. 极致的性能优化(代码看不懂有很大原因是加了很多 useMemo)(connect)

redux 原理

在看源码之前先贴上参考资料

createStore

它是创建整个状态树的关键,为什么说推荐在整个 app 中只创建一个状态树呢,多个状态树很难管理,而且大多数时候没有这个必要。一个状态树足以控制组件对数据变更做出反应。

createStore 内包含

  • dispatch 触发 reducer
  • subscribe 订阅 dispatch
  • getState 获取 state 状态树
  • replaceReducer 用来动态更改 reducer 函数
// preloadedState 是预置的 state,会在 redux 初始化的时候自动的被 reducer 的返回值覆盖
function createStore(reducer, preloadedState, enhancer) {
    // 只有 reducer 和 enhancer 的情况
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState;
        preloadedState = undefined;
    }
    if (typeof enhancer !== 'undefined') {
        return enhancer(createStore)(reducer, preloadedState);
    }
    // 当前的 reducer 函数,新创建一个变量是为了在 replaceReducer 调用时新的 reducer 函数复制给它
    let currentReducer = reducer;
    // 当前的 state
    let currentState = preloadedState;
    // 在 subscribe 中订阅的事件
    let currentListeners = [];
    let nextListeners = currentListeners;
    function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
            nextListeners = currentListeners.slice();
        }
    }
    // 获取当前 state
    function getState() {
        return currentState;
    }
    // 订阅 dispatch
    function subscribe(listener) {
        // 防止调用多次 unsubscribe 函数
        let isSubscribed = true;
        ensureCanMutateNextListeners(); // 暂时忽略
        nextListeners.push(listener); // 注册监听函数
        return function unsubscribe() {
            if (!isSubscribed) {
                return;
            }
            isSubscribed = false;
            ensureCanMutateNextListeners();
            const index = nextListeners.indexOf(listener);
            // unsubscribe 时把注册的函数从数组中剔除,防止后续 diapatch 时再次触发
            nextListeners.splice(index, 1);
            currentListeners = null;
        };
    }
    function dispatch(action) {
        // 让 reducer 函数计算新的 state,复制给 currentState
        currentState = currentReducer(currentState, action);
        // 调用注册的函数
        const listeners = (currentListeners = nextListeners);
        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i];
            listener();
        }
        return action;
    }
    // 更改当前的 reducer 函数
    function replaceReducer(nextReducer) {
        currentReducer = nextReducer;
        dispatch({ type: ActionTypes.REPLACE });
        return store;
    }
    // 初始化 state,可以看上面的 dispatch 函数,会导致 reducer 函数的最新结果赋值给 currentState
    dispatch({ type: ActionTypes.INIT });
    const store = {
        dispatch: dispatch,
        subscribe,
        getState,
        replaceReducer,
    };
    return store;
}

combineReducers

combineReducers 是把用户定义的多个 reducer 函数合并到一起,合并之后的新函数传递参数的方式和之前 reducer 函数相同。

/**
 * 把定义的多个 reducer 函数组合为一个 reducer 函数
 * 实现比较简单,reducers 对象遍历一遍,让每个 reducer 函数执行一次,比较前后的 state,并返回最新的 state
 * reducers = {counter: counterReducer}
 */
function combineReducers(reducers) {
    const reducerKeys = Object.keys(reducers);
    const finalReducers = {};
    for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i];
        if (typeof reducers[key] === 'function') {
            finalReducers[key] = reducers[key];
        }
    }
    const finalReducerKeys = Object.keys(finalReducers);
    // 返回一个新的 reducer 函数
    return function combination(state = {}, action) {
        let hasChanged = false;
        const nextState = {};
        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);
            nextState[key] = nextStateForKey;
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
        }
        hasChanged =
            hasChanged || finalReducerKeys.length !== Object.keys(state).length;
        return hasChanged ? nextState : state;
    };
}

compose

compose 就是把函数都组合起来 d = compose(a, b, c) => d(x) === a(b(c(x)))

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

举个例子:

type composeFunc = <T>(args: T) => T

export default function compose(...funcs: composeFunc[]) {
  if (funcs.length === 0) {
    return (arg: any) => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
const f1: composeFunc = args => {
  console.log('f1', args)
  return args
}
const f2: composeFunc = args => {
  console.log('f2', args)
  return args
}
const f3: composeFunc = args => {
  console.log('f3', args)
  return args
}
const f4: composeFunc = args => {
  console.log('f4', args)
  return args
}
const c = compose(
  f1,
  f2,
  f3,
  f4
)
const res = c('hello')
console.log('res', res)

打印的结果

f4 hello
f3 hello
f2 hello
f1 hello
res hello

从左到右依次是 f1f2f3f4 最后执行的结果是 f4f3f2f1,符合 d = compose(a, b, c) => d(x) === a(b(c(x)))

applyMiddleware

applyMiddleware 是应用 redux middleware 的地方。这个函数设计的非常精妙!!!

function applyMiddleware(...middlewares) {
  // const enhancer = compose(applyMiddleware(...middlewares))
  // enhancer(createStore)(reducer, preloadedState) reateStore 的返回值
  return createStore => (...args) => {
    const store = createStore(...args) // args => reducer, preloadedState
    // 这里定义一个假函数,传递给 middlewareAPI api 了,但是非常牛13 的是,middlewareAPI 通过闭包,可以获取到最新的 dispatch
    // 也就是可以获取到 compose 后的 dispatch
    let dispatch = () => { throw new Erro('不能在 middleware 构造之前调用 dispatch') }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    }
    //chain 和 dispatch 的生成可以参照下面写 middleware ,来反向推
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch) // 组合 middleware
    return {
      ...store,
      // 用新的 dispatch 替代 createStore 生成的 dispatch,让中间件发挥威力!!!
      dispatch,
    }
  }
}

我们来逐行分析:

  1. 它直接返回一个函数,函数的参数是 createStore,这和它被调用的地方对应 enhancer(createStore)(reducer, preloadedState)
  2. 先定义一个 dispatch,这个 dispatch 在 middlewarwe 构造完之前不能调用
  3. 生成 middlewareAPI 会被传到 middleware 中,它包含 getState 获取 state 状态树,以及 dispatchdispatch 用函数包裹了一层,这样可以利用闭包,让 middleware 内部使用构造好的 disaptch(这句很绕,其实就是使用最新的 dispatchdispatch 后面赋值为 compose(x)(y) 的返回值,是一个新的 dispatch),通过下面测试可以看到 dispatch 是使用最新赋值dispatch 由于是在函数内惰性获取值得到,所以一直都是最新的。
  4. const chain = middlewares.map(middleware => middleware(middlewareAPI)) 一个简单的 middleware 内部包含三个函数, map 把 middlewareAPI 传了进去,让内部的函数可以使用到 getStatedispatch,返回一个函数数组。
  5. dispatch = compose(...chain)(store.dispatch) !!!这句非常难看懂,compose(...chain) 把所有的 middleware 第二层函数前后进行包裹(上面 compose 有详细效果),其返回是一个函数 tmp1tmp1(store.dispatch) 返回一个函数,这个函数可以接收 action 作为参数,要注意 chain 内的每一个函数,其参数是 dispatch,而且其可以返回一个函数 t,函数 t 的参数是 action({type: xxx, payload: yyy})。

一个简单的 middleware 包含 3 层函数,(storeCtx) => (nextDispatch) => action => {}

function loggerMiddleware(storeContext) {
  return function(nextDispatch) { // nextDispatch => next ,和 Koa 中的 middleware 很像
    // 上面的 store.dispatch
    return function(action) {
    // dispatch 之前
      console.log('before', storeContext.getState())
      nextDispatch(action)
      // dispatch 之后
      console.log('after', storeContext.getState())
    }
  }
}

可前往 lxfriday.xyz/react-sourc… 打开开发者工具,点击增加、减少查看实际变化。

内部实现可参考 redux middleware 详解

洋葱模型的执行流程。

可以看到 compose(m1, m2) => m1(m2(dispatch))的效果,正好实现从 m1 内的 before 先开始,再到 m2 内的 after 最后执行。

下面我们来看看 thunk middleware 的实现逻辑 react-thunk,总共也没几行。

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}

createThunkMiddleware 的使用方式是:先createThunkMiddleware(...args) 把一些预置参数给 extraArgument ,返回的就是一个 middleware 函数,if 判断如果 action 是函数,则直接用 return 截断洋葱模型的执行流,不再继续往洋葱内部执行,可以看下面的图,图片来自知乎 @胡满川。

我写了一个假的网络请求 getWeather.js

import { weatherGetData } from './reducers/weather'
function getWeather(dispatch, getState, extraArgs) {
  console.log('state', getState())
  console.log('extraArgs', extraArgs)
  new Promise((res, rej) => {
    setTimeout(() => {
      res({
        data: {
          location: 'wuhan',
          condition: 'cloudy',
        },
      })
    }, 2000)
  }).then(({ data }) => {
    dispatch({
      type: weatherGetData,
      payload: data,
    })
  })
}
export default getWeather

reducer

const namespace = 'weather'
export const weatherGetData = `${namespace}/getData`

const INITIAL_STATE = {
  location: '',
  condition: '',
}

export default function(state = INITIAL_STATE, action) {
  switch (action.type) {
    case weatherGetData:
      return {
        ...state,
        ...action.payload,
      }
    default:
      return state
  }
}

然后配置文件

import thunk from 'redux-thunk'
import { createStore, applyMiddleware, compose } from '../lib/redux'
import rootReducer from '../reducers'
// 添加额外的信息
const enhancers = compose(applyMiddleware(thunk.withExtraArgument({ info: 'extra args when applyMiddleware' })))
export default function configStore() {
  const store = createStore(rootReducer, enhancers)
  return store
}

使用的地方 ReactRedux.js

点击 测试 thunk,会把 withExtraArgument 传递的参数打印出来,这个参数是所有的 thunk action 函数都可以获取到的,数据可以正常显示。

react-redux 原理

主要讲两个常用的功能:Providerconnect

Provider

Provider 内部实现和 React.createContext().Provider 一致,大概也就是这个思路。

const ReactReduxContext = React.createContext(null)
export function Provider({ store, children }) {
  return <ReactReduxContext.Provider value={store}>{children}</ReactReduxContext.Provider>
}

connect

接下来才是重点,connect!!!它的源码可以把人看吐。它里面包含了非常多性能优化,大量使用 useMemo。现在这个版本的内部实现使用的 functional component,内部使用 useReducer实现视图刷新。

看看用法 @connect(state => ({xxx}), (dispatch) => ({xxx, dispatch}), ...)(Comp)connect 大家用很多,参数就不说了,connect的返回值是一个 t 函数, t 函数的参数是一个 Component 而且 t 函数的返回值也是一个 Component。源码是在返回的 functional Component 内用 useContext 获取从 Provider 中提取的值。具体实现可以看下面的 模拟实现。

造 redux

我在 react-source-analyse 项目里面实现了 mini 版本的 reduxreact-redux,效果前往 MiniRedux

mini 版本的实现不包含复杂性能优化和错误提醒,尽可能直观的体现核心功能。

贴上源码

// redux.js
/**
 * redux 模拟实现
 */

// 初始化 redux 中的数据
const REDUX_INIT = '@@redux/INIT'

/**
 * 状态树创建的地方,返回 getState、subscribe、dispatch
 *
 * @param {*} reducer
 * @param {*} enhancer
 */
export function createStore(reducer, enhancer) {
  if (typeof enhancer === 'function') {
    return enhancer(createStore)(reducer)
  }

  let currentState
  let currentReducer = reducer
  let listeners = []

  function getState() {
    return currentState
  }

  function dispatch(action) {
    // 闭包,reducer 返回的 state 会是新的 state
    currentState = currentReducer(currentState, action)

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

  // 这个很重要,初始情况下,把 reducer 中的数据挂到 currentState 中
  dispatch({ type: REDUX_INIT })

  function subscribe(listener) {
    // 把传进来的订阅函数推入 listeners 数组
    listeners.push(listener)
    // 返回的取消订阅的函数
    return function unsubscribe() {
      const index = listeners.indexOf(listener)
      // 删除订阅器
      listeners.splice(index, 1)
    }
  }

  return {
    getState, // 获取 state
    dispatch, // 触发数据变更
    subscribe, // 刷新 connect 组件的时候用到 dispatch 之后触发视图强制渲染
  }
}

/**
 * 组合 reducer 的函数,处理有多个 reducer 的时候如何把它们组合成一个大 reducer,
 * 其实就是把 action 分发到每个 reducer,匹配对应的一个 switch,然后返回一个新的 state
 * 所有的 reducer 函数都符合 `function (state, action)` 签名
 */
export function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  return function combination(state = {}, action) {
    const nextState = {} // 这个地方做了简化的处理,实际上为了性能优化(useMemo),能不返回新 state 就一定不要返回它
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i] // reducer 函数名
      const reducer = reducers[key] // 当前的 reducer 函数
      const previousStateForKey = state[key] // 调用 reducer 之前的 state
      // 这句的调用有前提,reducer 函数内 switch case 语句 必须有 default 条件,要返回传入的 state
      // 这样就表示 reducer 调用没有更改 state
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey
    }

    // 如果发生了变更,大 state 也会返回新生成的 state
    return nextState
  }
}

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

export function applyMiddleware(...middleware) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error('暂时不能调用')
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    }

    const chains = middleware.map(m => m(middlewareAPI))
    dispatch = compose(...chains)(store.dispatch)

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

造 react-redux

mini 版本的 react-redux 实现,掘金对 jsx 的代码高亮一直没显示正常过,可以去 github 看有高亮的代码。connect 中有对 dispatch 的订阅(subscribe),当 disptch 的时候会把通过 subscribe 注册的监听函数全部执行一遍,这样 connect 内部就可以知道 state 变化了,再使用 useReducer 刷新。

// react-redux.js
/**
 * 模拟实现 react-redux
 */

import React, { createContext, useContext, useReducer, useEffect } from 'react'
// 贯穿 App 的 context
const ReactReduxContext = createContext(null)

/**
 * 它会被当成一个 React 组件
 */
export function Provider({ store, children }) {
  return <ReactReduxContext.Provider value={store}>{children}</ReactReduxContext.Provider>
}

const initStateUpdates = () => [null, 0]
function storeStateUpdatesReducer(state, action) {
  console.log('storeStateUpdatesReducer', state, action)

  const [, updateCount] = state
  return [action.payload, updateCount + 1]
}

/**
 * 这是一个 HOC
 * connect 源码的实现非常复杂,Dan 写过一个简单实例
 * @link https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e
 */
export function connect(mapStateToProps, mapDispatchToProps) {
  // 用法 @connect(state => xxx, (dispatch) => xxx)(Comp)
  return function(WrappedComponent) {
    // 组件
    function ConnectFunc(props) {
      const store = useContext(ReactReduxContext)
      const [, forceComponentUpdateDispatch] = useReducer(storeStateUpdatesReducer, null, initStateUpdates)
      const state = store.getState()
      // 订阅 dispatch 行为
      useEffect(() => {
        const unsubscribe = store.subscribe(() => {
          forceComponentUpdateDispatch({
            type: '@@redux/STORE_UPDATED',
            payload: {
              latestStoreState: state,
            },
          })
        })
        return () => {
          // 每次组件重新渲染的时候取消上一次的订阅,否则订阅数会一直增加
          unsubscribe()
        }
      })

      return <WrappedComponent {...props} {...mapStateToProps(state, props)} {...mapDispatchToProps(store.dispatch, props)} />
    }

    return ConnectFunc
  }
}

上面 connect 函数的实现有点复杂,它的用法是 @connect(state => xxx, (dispatch) => xxx)(Comp),所以第一次执行会返回一个函数 func1, func1 执行返回一个 React 组件,在 functional component 内部就可以使用 Hooks 函数获取 Context 以及使用 useReducer

Redux 作者 Dan 也写了一个 connect 的实现思路 connect.js

// connect() is a function that injects Redux-related props into your component.
// You can inject data and callbacks that change that data by dispatching actions.
function connect(mapStateToProps, mapDispatchToProps) {
  // It lets us inject component as the last step so people can use it as a decorator.
  // Generally you don't need to worry about it.
  return function (WrappedComponent) {
    // It returns a component
    return class extends React.Component {
      render() {
        return (
          // that renders your component
          <WrappedComponent
            {/* with its props  */}
            {...this.props}
            {/* and additional props calculated from Redux store */}
            {...mapStateToProps(store.getState(), this.props)}
            {...mapDispatchToProps(store.dispatch, this.props)}
          />
        )
      }
      
      componentDidMount() {
        // it remembers to subscribe to the store so it doesn't miss updates
        this.unsubscribe = store.subscribe(this.handleChange.bind(this))
      }
      
      componentWillUnmount() {
        // and unsubscribe later
        this.unsubscribe()
      }
    
      handleChange() {
        // and whenever the store state changes, it re-renders.
        this.forceUpdate()
      }
    }
  }
}

// This is not the real implementation but a mental model.
// It skips the question of where we get the "store" from (answer: <Provider> puts it in React context)
// and it skips any performance optimizations (real connect() makes sure we don't re-render in vain).

// The purpose of connect() is that you don't have to think about
// subscribing to the store or perf optimizations yourself, and
// instead you can specify how to get props based on Redux store state:

const ConnectedCounter = connect(
  // Given Redux state, return props
  state => ({
    value: state.counter,
  }),
  // Given Redux dispatch, return callback props
  dispatch => ({
    onIncrement() {
      dispatch({ type: 'INCREMENT' })
    }
  })
)(Counter)


参考文章


欢迎大家关注我的掘金和公众号,算法、TypeScript、React 及其生态源码定期讲解。