React-redux总结

1.Provider

function Provider({store, context, children}){
    //contextValue初始化
    const contextValue = useMemo(()=>{
        const subscription = new Subscription(store);
        subscription.onStateChange = subscription.notifyNestedSubs;
      //自定义订阅模型供子组件订阅
        return {
            store,
            subscription
        }
    }, [store])
    const previosState = useMemo(()=> store.getState(),[store]);
    useEffect(()=>{
        const { subscription } = contextValue;
      //根节点订阅原始store,子节点订阅父组件中的subscription
        subscription.trySubscribe();
        if(previosState !== store.getState()){
          //订阅模型形成网状结构,保证订阅函数的执行顺序为父节点到子节点的网状结构,防止子节点的订阅者先触发导致过期props问题。
            subscription.notifyNestedSubs();
        }
        return ()=>{
            subscription.tryUnsubscribe();
            subscription.onStateChange = null;
        }
    },[contextValue, previosState])
    const Context =context || ReactReduxContext;
    return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

上面的逻辑可以简述如下:

  • 新建一个订阅模型subscription供子组件订阅
  • 在useEffect中subscription调用subscription.trySubscribe()订阅父组件传来的store。
  • 当store由于dispatch触发订阅函数时,执行subscription.notifyNestedSubs,由于子组件订阅的是父组件的subscription,子组件触发订阅函数

    ................

  • 将改造后的store通过contextAPI传入子组件。

2. Connect

connect可以将store中的数据和方法进行处理后通过props传入其包裹的组件中。

2.1 mapStateToProps: (state, ownProps) => stateProps

mapStateToProps方法主要用来对store中数据进行reshape。因为每次store变化都会触发所有connect中的mapStateToProps函数执行,所以该函数应该运行的足够快以免影响性能。必要的时候可以使用selector库来避免不必要的计算(相同输入的情况下不进行再次计算,而是使用上一次的缓存值)。

react-redux进行了很多优化以避免不必要的重复渲染,

(state) => stateProps (state, ownProps) => stateProps
mapStateToProps 执行条件: store state 变化 store state 变化或者ownProps变化
组件重新渲染条件: stateProps变化 stateProps变化或者ownProps变化

从上表可以总结一些tips:

  • 判断stateProps变化是采用shallow equality checks比较的, 每次执行(state, ownProps) => stateProps即使输入值一样,如果 stateProps中的每个field返回了新对象,也会触发重新渲染。可以使用selector缓存上一次的值来避免stateProps变化。
  • 当store state没有变化时,mapStateToProps不会执行。connect在每次dispatch后,都会调用store.getState()获取最新的state,并使用lastState === currentState判断是否变化。而且在 redux combineReducers API中也做了优化,即当reducer中state没有变化时,返回原来的state。
  • ownProps也是mapStateToProps执行和组件重新渲染的条件,所以能不传的时候不要传。

2.2 mapDispatchToProps

mapDispatchToProps可以将store.dispatch或者dispatch一个action的方法((…args) => dispatch(actionCreator(…args)))传递到其包裹的组件中。mapDispatchToProps有多种用法:

  • 不传递mapDispatchToProps时,将会将dispatch方法传递到其包裹的组件中。
  • mapDispatchToProps定义为函数时,(dispatch,ownProps)=>dispatchProps

    const increment = () => ({ type: 'INCREMENT' })
    const decrement = () => ({ type: 'DECREMENT' })
    const reset = () => ({ type: 'RESET' })
    
    const mapDispatchToProps = dispatch => {
      return {
        // dispatching actions returned by action creators
        increment: () => dispatch(increment()),
        decrement: () => dispatch(decrement()),
        reset: () => dispatch(reset())
      }
    }

    redux中提供了bindActionCreators接口可以自动的进行actionCreators到相应dispatch方法的转换:

    import { bindActionCreators } from 'redux'
    
    const increment = () => ({ type: 'INCREMENT' })
    const decrement = () => ({ type: 'DECREMENT' })
    const reset = () => ({ type: 'RESET' })
    
    function mapDispatchToProps(dispatch) {
      return bindActionCreators({ increment, decrement, reset }, dispatch)
    }
  • mapDispatchToProps定义为action creators键值对时,connect内部会自动调用bindActionCreators将其转化为dispatching action函数形式(dispatch => bindActionCreators(mapDispatchToProps, dispatch))。

3. batch

默认情况下,每次dispatch都会执行connect函数,并执行接下来可能的重复渲染过程,使用batchAPI,可以将多次dispatch合并,类似setState的合并过程。

import { batch } from 'react-redux'

function myThunk() {
  return (dispatch, getState) => {
    // should only result in one combined re-render, not two
    batch(() => {
      dispatch(increment())
      dispatch(increment())
    })
  }
}

4. hooks

可以在不用connect包裹组件的情况下订阅store或dispatch action。

4.1 useSelector()

下面是对useSelector的简单实现:

const useSelector = selector => {
  const store = React.useContext(Context);
  const [, forceUpdate] = React.useReducer(c => c + 1, 0);
  const currentState = React.useRef();
  // 在re-render阶段更新state使得获取的props不是过期的
  currentState.current = selector(store.getState());

  React.useEffect(() => {
    return store.subscribe(() => {
      try {
        const nextState = selector(store.getState());

        if (nextState === currentState.current) {
          // Bail out updates early
          return;
        }
      } catch (err) {
        // Ignore errors
        //忽略由于过期props带来的计算错误
      }
            //state变化需要重新渲染
      // Either way we want to force a re-render(与其他订阅者中的forceUpdate合并执行)
      forceUpdate();
    });
  }, [store, forceUpdate, selector, currentState]);

  return currentState.current;
};

useSelector中新旧state的对比使用===,而不是connect中的浅比较,所以selector返回一个新的对象会导致每次重新渲染。对于这个问题,可以使用多个selector返回基本类型的值来解决;或者使用reselect库缓存计算结果;最后还可以传递useSelector的第二个参数,自定义新旧state的比较函数。

connect中会对新旧state和props值进行比较来决定是否执行mapStateToProps,但是useSelector中没有这种机制,所以不会阻止由于父组件的re-render导致的re-render(即使props不变化),这种情况下可以采用React.memo优化。

4.2 useDispatch()

返回dispatch的引用

const dispatch = useDispatch()

4.3 useStore()

返回store的引用

const store = useStore()

karl
78 声望5 粉丝

« 上一篇
前端路由库
下一篇 »
React Scheduler