Redux如何实现state变化触发页面渲染?

5,545 阅读4分钟

image.png React Context提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法,这样我们就不需要通过层层的props传递实现通信,这大大降低了我们的代码复杂度,同时也带来了新的问题

  1. 组件变得很难复用
  2. Context 对象 提供的Provider组件允许消费组件订阅 context 的变化,一旦变化它内部的所有消费组件都会重新渲染,会产生性能问题。

image.png

虽然第二点我们可以使用useMemo处理:

function Button() {
    let {theme} = useContext(appContext)
    
    return useMemo(() => {
        return <Children class={theme} />
    }, [theme])
}

但是手动优化和管理依赖必然会带来一定程度的心智负担,那么react-redux作为社区知名的状态管理库,肯定被很多大型项目所使用,大型项目里的状态可能分散在各个模块下,它是怎么解决上述的性能缺陷的呢?

我们知道 Redux 是一个单一的状态机,它只关注state的变化,至于视图层怎么变化,关键在于React-redux。

先看一下我们平时在代码中是怎么使用:(完整代码👇

import {createStore} from 'redux'
import reducer from './reducer'
import {Provider} from 'react-redux'

let store = createStore(todoApp);

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

function App() {
  return (
    <>
      <WrapChildFunc />
      <WrapChildClass />
    </>
  )
}

如上代码,react-redux提供了Provider组件,接收一个store对象(redux的store对象),重点源码如下

function Provider({ store, context, children }) {
  const contextValue = useMemo(() => {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription
    }
  }, [store])

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

  useEffect(() => {
    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>        // 渲染子级元素,使整个应用成为Provider的子组件          
}

其实Provider主要做了两件事:

  1. 在原应用组件上包裹一层,使整个应用成为Provider的子组件
  2. 接收redux的store作为props,通过context对象传递给所包含的消费组件 还是利用Context对象与其他组件共享数据

Class组件

class ChildClass extends React.Component {
    constructor(props) {
        super(props)
        console.log('class', this.props, this.props.children)
    }
    render() {
        const {ClassNum, addClick, reduceClick} = this.props
        return (
            <>
                <div className="App">{ClassNum}</div>
                <div>Class组件</div>
                <button onClick={addClick}>+</button>
                <button onClick={reduceClick}>-</button>
            </>
        )
    }

}

const mapStateToProps = state => {
    return {
        ClassNum: state.ClassNum
    }
}

const mapDispatchToProps = dispatch => {
    return {
        addClick: () => {
            dispatch({type: 'CLASS_ADD'})
        },
        reduceClick: () => {
            dispatch({type: 'CLASS_REDUCE'})
        }
    }
}

// 使用connect容器组件包裹ChildClass UI组件
export const WrapChildClass = connect(mapStateToProps, mapDispatchToProps)(ChildClass)

connect模块就是一个高阶组件,主要作用是:

  1. connect通过context获取Provider中的store,通过store.getState()获取state tree
  2. connect模块返回函数wrapWithComponent
  3. wrapWithConnect返回一个ReactComponent对象 Connect,Connect重新render外部传入的原组件WrappedComponent(UI组件),并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent 具体代码比较复杂,大家感兴趣的可以看一下源码

Function 组件

我们主要分析下Hooks写法

export default function Child () {
    // 获取redux中state树中指定的值
    const {FuncNum} = useSelector((state: stateType) => {
        console.log('Func', state)
        return {
            FuncNum: state.FuncNum
        }
    })
    // 饮用store中的dispatch 方法
    const dispth = useDispatch()

    const addClick = useCallback(
        () => dispth({type: 'FUNC_ADD'}),
        [FuncNum],
    )

    const reduceClick = useCallback(
        () => dispth({type: 'FUNC_REDUCE'}),
        [FuncNum],
    )
    
    return useMemo(() => (
        <div>
            <div className="App">{FuncNum}</div>
            {Math.random()}
            <div>Func组件</div>
            <button onClick={addClick}>+</button>
            <button onClick={reduceClick}>-</button>
        </div>
    ), [FuncNum])
}

可以看到,我们通过useSelector获取store中state对象对应的数据,再通过useDispatch方法获取到dispatch的引用

useDispatch
export function createDispatchHook(context = ReactReduxContext) {
  const useStore =
    context === ReactReduxContext ? useDefaultStore : createStoreHook(context)

  return function useDispatch() {
    const store = useStore()
    return store.dispatch
  }
}

可以看到useDispatch只返回了dispatch的引用,并没有做其他的事情,那么我们dispatch(Action)后,state变化,react是怎么知道需要更新视图的?关键就在于useSelector

useSelector
function useSelectorWithStoreAndSubscription(
  selector,
  equalityFn,
  store,
  contextSub
) {
  // 强制渲染 reducer: s => s+1,  state 初始值: 0; forceRender ===> dispatch
  const [, forceRender] = useReducer(s => s + 1, 0)

  // 创建订阅函数
  const subscription = useMemo(() => new Subscription(store, contextSub), [    store,    contextSub  ])

  const latestSubscriptionCallbackError = useRef()
  // select函数
  const latestSelector = useRef()
  // store中的state
  const latestStoreState = useRef()
  // select函数返回state
  const latestSelectedState = useRef()

  const storeState = store.getState()
  let selectedState

  try {
    if (
      selector !== latestSelector.current ||
      storeState !== latestStoreState.current ||
      latestSubscriptionCallbackError.current
    ) {
      // useSelector 选择的state
      selectedState = selector(storeState)
    } else {
      selectedState = latestSelectedState.current
    }
  } catch (err) {
    ...
  }

  // effect hook
  useIsomorphicLayoutEffect(() => {
    latestSelector.current = selector
    latestStoreState.current = storeState
    latestSelectedState.current = selectedState
    latestSubscriptionCallbackError.current = undefined
  })

  // effect hook
  useIsomorphicLayoutEffect(() => {
    function checkForUpdates() {
      try {
        const newSelectedState = latestSelector.current(store.getState())

        // 判断前后选择的state是否相等
       if (equalityFn(newSelectedState, latestSelectedState.current)) {
          return
        }

        latestSelectedState.current = newSelectedState
      } catch (err) {
        latestSubscriptionCallbackError.current = err
      }

      forceRender()
    }

    // stateChange的回调
    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    checkForUpdates()

    return () => subscription.tryUnsubscribe()
  }, [store, subscription])

  return selectedState
}


export function createSelectorHook(context = ReactReduxContext) {
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () => useContext(context)
  return function useSelector(selector, equalityFn = refEquality) {
    const { store, subscription: contextSub } = useReduxContext()

    const selectedState = useSelectorWithStoreAndSubscription(
      selector,
      equalityFn,
      store,
      contextSub
    )
    return selectedState
  }
}
  • 关键方法: const [, forceRender] = useReducer(s => s + 1, 0): 利用useReducer定义了一个计数器,用于强制渲染此组件

    checkForUpdates:store变化后订阅函数触发的处理逻辑

  • 关键流程:初始化

    1. 利用useSelector传入的selector函数获取Redux中store对应的值
    2. 定义一个latestSelectedState,用于保存上一次selector返回的值
    3. 定义state变化的处理函数checkForUpdates
    4. 利用store.subscribe订阅一次redux的store,当下次store变化后,触发订阅函数执行checkForUpdates
  • 关键流程:更新

    1. 当用户dispacth触发了store变化后,订阅函数执行checkForUpdates
    2. 通过store.getState()获取最新的state值,通过equalityFn函数比较newSelectedState和latestSelectedState,如果有变化就执行forceRender,触发react创建update对象,强制渲染;否则直接return