redux和react-redux原理分析

1,009 阅读2分钟

文章介绍录制文件

最近看了看redux和react-redux的源码,总算理清楚redux的执行机制以及如何与react关联起来的。

redux其实只是JavaScript状态管理容器,与具体框架无关。如果需要,vue框架也是能用的,当然,这不是首要选择。

redux的实现

redux库的核心文件是:createStore.js,combineReducer.js,applyMiddleware.js,compose.js,另外的就是redux工具文件了。

其中主函数是createStore函数。

源码截图

createStore

一个数据状态管理容器(也就是store)的实现,需要、存储state的地方、修改state的方法、监听state修改、以及redux进行扩展的中间件。

源码中的createStroe.js文件导出一个函数function createStore(reducer,preloadedState,enhancer)

  1. reducer:计算函数,用来计算生成新的state
  2. preloadedState,初始化state
  3. enhancer:增强器器函数,用于增强createStore函数

createStore函数返回一个store对象,下面是伪代码说明。

interface IStore{
	dispatch:(action:any)=>action, 
    subscribe:(listen:Function)=>Function,
    getState:()=>void,
    replaceReducer,
    [$$observable]: observable, //不重要
}
interface ICreateStroe{
	(reducer:Function,preloadedState:Object,enhancer:Function)=>IStore;
}
const store:IStrore=createStore();

分析源码可以发现,如果传入enhancer参数,则返回调用该函数的返回值。最终返回的是创建好的stroe对象。

其中重要的是dispatch方法和subscribe监听方法。

dispatch函数

dispatch默认传入一个action(普通对象,该对象要求有type属性)。修改完state后会触发所有的监听函数。

dispatch函数中会调用reducer函数,用于修改state。

subscribe订阅方法

subscribe监听方法,用来向store中添加监听函数,后期dispatch将会调用。返回取消监听函数。createStore函数中对于监听函数队列的处理还是很有意思的,能够保证追加和取消的安全,哪怕是在监听中再取消某个监听函数,也能保证安全。

reducer函数combineReducers.js

reducer函数是一个纯函数,单纯用来计算生成新的state。通常项目中会有很多需要管理的数据,需要很多reducer。因此需要修改合并所有的reducer函数成一个完整的reducer函数。

在redux源码中,提供的combineReducers.js文件中,导出了combinReducers方法,该方法接口如下

//返回值就是最终的reducer。注意,每个dispatch会导致所有的reducer都触发一遍,而且action的type不能重复
function combineReducers(reducers:Object) => Function 

中间件middleware和applyMiddleware

通常我们需要对redux进行一些扩展,需要通过middleware进行操作。

所谓的中间件,就是对redux中的state修改过程的增强,例如可以追加日志输出啊,特殊的埋点啊等等。

中间件接受store中的dispatch和getState,用来做各种额外的骚操作。

而实现的核心就在于重写了dispatch,这个工作是由redux提供的applyMiddleware函数负责处理的。

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) //这个是组装了每一个函数,按照数组的顺序,从右向左组装,dispatch的执行时从右向左的顺序

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

在applyMiddleware函数中,compose函数很有意思,这个函数是redux提供的工具函数,具体细节是:

//参数是收敛后的执行完的中间件函数的返回值(这个返回值也是函数)
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return (arg) => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  // 使用了数组的reduce方法,从右向左的合成最终的dispatch函数
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

这种中间件串联的方法,又叫做洋葱模型。即最终合成函数像是从外向里层层包裹,执行时却是从内向外(因为总是对参数先行求值)调用中间件。

至此,就是redux原理的简单分析了。

异步数据流方案redux-thunk

分析完redux的源码可以看出来,redux对于数据的处理都是同步执行的。然而我们在项目中,常常是需要异步更新状态,这就呼唤新的解决方案。

恰巧redux的中间件机制给我们留下了实现的机会。

在很多的redux异步处理方案中,react-thunk是一个很简单的解决方案。源码异常简洁。

const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
    ({ dispatch, getState }) =>
    next =>
    action => {
      // The thunk middleware looks for any functions that were passed to `store.dispatch`.
      // If this "action" is really a function, call it and return the result.
      if (typeof action === 'function') {
        // Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
        return action(dispatch, getState, extraArgument)
      }

      // Otherwise, pass the action down the middleware chain as usual
      return next(action)
    }

对,核心代码就是这么长。这里有个函数柯里化的概念。

柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术

作用是接受redux默认的dispatch和getState方法, 并返回一个接受后续步骤next的柯里化函数。

由于重写了dispatch,在调用disaptch的时候,如果传递进来的action是一个函数,则返回函数调用的返回值,否则执行后续步骤next函数的方法。

redux在react的应用react-redux

react-redux库的作用是处理在react库中使用redux。

Provider组件

使用Provider包裹项目根组件,起到顶层state的作用。

Provider的实现原理是react中的context注入,其中的store是通过props传入。

import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'

const store = createStore(rootReducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

//react-redux Provider源码
function Provider({ store, context, children }) {
  const contextValue = useMemo(() => {
    const subscription = createSubscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

  const previousState = useMemo(() => store.getState(), [store])

  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue
    subscription.trySubscribe()

    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

在将store作为顶部的state保存下来之后,具体的组件又是如何同顶层的state关联的呢?换句话说,redux提供的数据,其单向数据流是怎么保持的,内部子孙组件如何通知store发生改变,又是怎么绑定具体的state呢。

两个方案:高阶函数connect和hook方案。

connect

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 高阶组件是参数为组件,返回值为新组件的函数

组件绑定state和dispatch

connect函数接受四个参数,主要用到的是前两个,mapStateToProps和mapDispatchToProps。

这两个函数/对象,将需要的state和触发action的函数传递到connect,其内部会处理最终注入到组件的props中。如果想要知道具体的注入的流程,需要看一下react-redux的源码了,主要在selectFactory.js和connectAdvanced.js中最终调用usePureOnlyMemo生成的actualChildProps对象,该对象由相关的state和分发action的封装函数。

//源码示例
connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
)=> conectHOC(selectorFactory,options)
  //使用示例
class connectDemo extends Component {
  render() {
    const { userList, createUser, dispatch } = this.props;
    return (
      <div>
        {userList.map((user) => (
          <div key={user.id} style={{ marginBottom: 20 }}>
            <span
              style={{ marginRight: 10 }}
            >{`${user.id}: ${user.name}`}</span>
          </div>
        ))}
        <button
          onClick={() => {
            createUser({
              id: uuid(),
              name: '名字' + Math.floor(Math.random() * 100),
            });
          }}
        >
          新增用户
        </button>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    userList: state.userList,
  };
}
const mapDispatchToProps = {
  createUser,//直接传递一个对象,在connect内部处理已经在外面包裹了dispatch调用,因此在上方组件中可以直接调用该方法,不需要显示的调用dispatch,显示调用就错了
};
export default connect(mapStateToProps, mapDispatchToProps)(connectDemo);

组件触发state更新

组件触发更新同hook方案是一致的,都是内部初始化state的时候想redux添加了订阅,当dispatch分发action的时候,触发订阅函数,内部调用useReducer,从而走react的更新流程。

hook方案

组件绑定state

使用useSelector,这个hook接受一个函数,该函数的入参是整体的state,返回值是需要绑定的子state,hook内部根据穿入的函数从整体的state读取了相关的数据并返回。当store维护的数据变化,触发监听函数,调用useReducer刷新对应的组件

组件触发state更新

使用useDispatch,其实这个hook就是返回的store内部的dispatch(当然是用了中间件,真是重写后的dispatch),通知store更新state后,redux触发监听函数,这个监听函数就是是用useSelector这个hook的时候绑定的,此时会调用useReducer的第二个返回值,刷新组件。