前端造轮子【6】- 简易 react-redux

147 阅读4分钟

上一章中,我们已经学习了 redux 内部的原理,今天我们就更进一步,来看看作为 react 特化的 react-redux。

同样,先来看看应用。

react-redux 主要有两类 api

  • 用于提供数据:Provider
  • 用于接收数据:connect,hooks

其中 connect 是作为 react 16.8 以前的主要手段,而 hooks(useSelector、useDispatch)则是之后新推出的 api。

下面我们分别来看看如何应用吧:

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './store'
import {Provider} from "react-redux";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)
// ReactReduxPage.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
  return {
    count: state.count,
  }
}

function mapDispatchToProps(dispatch) {
  let creators = {
    add: () => ({ type: 'ADD', payload: 1 }),
  }
  creators = bindActionCreators(creators, dispatch)
  return { dispatch, ...creators }
}

@connect(mapStateToProps, mapDispatchToProps)
class ReactReduxPage extends Component {
  render() {
    const { count, add } = this.props
    return (
      <>
        <div>React Redux Page</div>
        <p>{count}</p>
        <button onClick={add}>add</button>
      </>
    )
  }
}
export default ReactReduxPage

这里需要注意的是,mapDispatchToProps 还可以传入一个 object:

{
	add: () => ({type: "ADD", payload: 1}),
}

这样和作为 function 的区别在于,object 无法在 props 中直接获取 dispatch。

在 16.8 之后,官方更加推荐使用 hooks 的写法:

import React, { useCallback } from 'react'
import { useSelector, useDispatch } from 'react-redux'

export default function ReactReduxHooksPage() {
  const count = useSelector((state) => state.count)
  const dispatch = useDispatch()

  const add = useCallback(() => {
    dispatch({
      type: 'ADD',
      payload: 1,
    })
  }, [])

  return (
    <div>
      <h3>React Redux Hooks Page</h3>
      <p>{count}</p>
      <button onClick={add}>add</button>
    </div>
  )
}

可以看到,与纯 redux 相比,在 react-redux 中,我们不必再手动的监听,更新视图,对于数据的获取也变得更加方便简洁,那么接下来我们就来看看,react-redux 是怎么实现的吧。

Provider

看到 Provider 的使用方法以及功能,第一时间想到的应该是 react 的 context:

import React, { createContext } from 'react'

const Context = createContext()

export const Provider = ({ store, children }) => {
  return <Context.Provider value={store}>{children}</Context.Provider>
}

接下来我们替换 Provider,并且通过 console 查看一下是否能够成功获取到 store:

看样子是没有问题的。

connect

数据提供没有问题,那么我们来到数据获取。

首先 connect 接收两个(实际上在 react-redux 中是 4 个)可选参数,并且在执行之后能够修饰 react 组件,那么它的架子还是可以比较轻松的搭出来的:

const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
  return <WrappedComponent {...props} />
}

我们先来处理 state,思路很简单:

mapStateToProps 是一个函数,在执行之后获取对应的 state。那么我们可以先通过 context 获取 store,然后将 getState 传给 mapStateToProps,最后将 state 传给 WrappedComponent 即可。

import React, { useContext } from 'react'
import { Context } from './Provider'

const getStateProps = (mapStateToProps) => (getState) => {
  return mapStateToProps(getState())
}

export const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
  const store = useContext(Context)
  const { getState } = store

  const stateProps = getStateProps(mapStateToProps)(getState)

  return <WrappedComponent {...props} {...stateProps} />
}

接下来是 dispatch,思路与上面是类似的,需要注意的是,这里要区分一下 mapDisPatchToProps 的类型:

const getDispatchProps = (mapDisPatchToProps) => (dispatch) => {
  let dispatchProps = {
    dispatch,
  }

  if (typeof mapDisPatchToProps === 'object') {
    dispatchProps = bindActionCreators(mapDisPatchToProps, dispatch)
  } else if (typeof mapDisPatchToProps === 'function') {
    dispatchProps = mapDisPatchToProps(dispatch)
  }

  return dispatchProps
}

export const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
  const store = useContext(Context)
  const { getState, dispatch } = store

  const stateProps = getStateProps(mapStateToProps)(getState)
  const dispatchProps = getDispatchProps(mapDisPatchToProps)(dispatch)

  return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
}

最后,我们需要在 state 改变的时候重新 render,具体技术再 redux 篇中已经介绍过了,这里就不再赘述:

export const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
  const store = useContext(Context)
  const [, forceUpdate] = useReducer((x) => x + 1, 0)
  const { getState, dispatch, subscribe } = store

  const stateProps = getStateProps(mapStateToProps)(getState)
  const dispatchProps = getDispatchProps(mapDisPatchToProps)(dispatch)

  useEffect(() => {
    const unSubscribe = subscribe(() => {
      forceUpdate()
    })
    return () => {
      unSubscribe && unSubscribe()
    }
  }, [])

  return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
}

最终效果如下:

hooks

hooks 的 api 有两个,分别是 useSelector 和 useDispatch,使用上面已经简单介绍过了,这里来看看实现。

useSelector

这个 hook 接收一个查询函数,并且返回对应的 state,思路如下:

  • 获取 store
  • 获取 getState
  • 查询 state
  • 返回 state

直接按照这个思路编写代码即可:

import { useContext } from 'react'
import { Context } from './Provider'

const useStore = () => {
  return useContext(Context)
}

export const useSelector = (selector) => {
  const store = useStore()
  const { getState } = store
  const state = selector(getState())
  return state
}

useDispatch

同上,思路非常简单:

  • 获取 store
  • 获取 dispatch
  • 返回 dispatch
export const useDispatch = () => {
  const store = useStore()
  return store.dispatch
}

最后就是更新,那么更新是放在 useSelector 还是 useDispatch 中呢?

显然,状态修改的时候才需要更新,而很多获取 dispatch 的操作可能并没有修改状态,那么更新的位置也就很明显了:

export const useSelector = (selector) => {
  const [, forceUpdate] = useReducer((x) => x + 1, 0)

  const store = useStore()
  const { getState } = store

  useEffect(() => {
    const unSubscribe = subscribe(() => {
      forceUpdate()
    })
    return () => {
      unSubscribe && unSubscribe()
    }
  }, [])
  const state = selector(getState())
  return state
}

拓展

到这里,一个简单的 react-redux 实际上已经实现了,但还有一点美中不足的地方:

我们知道 useEffect 是执行是会延后的,那么假如在组件渲染完成之后,到 useEffect 执行之间发生了状态的改变,显然是不会触发 render 的,那么这里我们可以借助 react 提供的另一个 api:useLayoutEffect。