Skip to content

Latest commit

 

History

History
190 lines (154 loc) · 8.52 KB

再谈Connect设计.md

File metadata and controls

190 lines (154 loc) · 8.52 KB

阅读本文前,请先阅读前文数据流前篇

回顾以及引入问题

在之前的设计中,我们使用 useReducer 获得了一个强制更新函数(forceComponentUpdateDispatch),然后在store.subcribe回调函数中执行

export const connect = (mapStateToProps, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {
  const { ...wrapperProps } = props;
  const context = useContext(ReactReduxContext);
  const { store } = context; // 解构出store
  const state = store.getState(); // 拿到state
  //使用useReducer得到一个强制更新函数
  const [, forceComponentUpdateDispatch] = useReducer((count) => count + 1, 0);
  // 订阅state的变化,当state变化的时候执行回调
  store.subscribe(() => {
    forceComponentUpdateDispatch();
  });
  // 执行mapStateToProps和mapDispatchToProps
  const stateProps = mapStateToProps?.(state);
  const dispatchProps = mapDispatchToProps?.(store.dispatch);
  // 组装最终的props
  const actualChildProps = Object.assign(
    {},
    stateProps,
    dispatchProps,
    wrapperProps
  );
  return <WrappedComponent {...actualChildProps} />;
};

上述代码已经实现了 store 中数据改变时,对应使用 connect 包裹的组件能够获得对应数据,但是存在一个更新顺序的问题。

connect

在前文中,我们提及到 React 是单向数据流,props 都是父组件传递给子组件的。一旦我们引入了 redux 后,假设父子组件都会引用了同一个变量count,子组件根本不会从父组件拿该参数,而是直接从 redux 中读取,这使得 React 的原本父→子的单向数据流被打破了。

再说到更新问题,在 React 中,如果一个共同变量变化了,那必然是父组件先更新,再把数据传给子组件做更新。但是 redux 里,数据变成 redux→父redux→子,父子组件完全根据 redux 的数据做独立更新,不能完全保证父组件先更新,子组件再更新。react-redux 为了保证更新顺序引入了一个监听者类Subscription

Subscription类

Subscription需要做什么?线上代码

  1. 实现发布订阅,处理所有state的回调
  2. 需要判断当前连接 redux 的组件是否为第一个连接 redux 的组件,如果当前组件就是连接 redux的根组件,它state回调直接注册到 redux store;同时创建一个Subscription实例(subscription)并且通过context传递给子级
  3. 如果当前组件不是根组件,说明已经有组件注册到了 redux store 了,那在子组件中可以拿到通过context传递的subscription(由于是父组件的监听类又称为parentSub),那么当前子组件的回调会注册到parentSub上。并且会新建一个Subscription实例,在context上继续传递,那么当前组件的子组件回调会注册到当前组件的Subscription实例上
  4. state变化了,根组件注册到 redux store 的回调会更新根组件,根组件会手动更新子组件的回调,子组件的回调执行更新子组件,子组件会执行subscription上注册的回调,触发孙子组件更新...这样子就实现了一层一层的组件更新,保证了父→子的更新顺序
export class Subscription {
  constructor(store, parentSub) {
    this.store = store;
    this.parentSub = parentSub;
    this.listeners = [];
    this.handleChangeWrapper = this.handleChangeWrapper.bind(this);
  }
  //当前组件注册
  addNestedSub(listener) {
    this.listeners.push(listener);
  }
  //通知监听者
  notifyNestedSub() {
    this.listeners.forEach((listener) => listener());
  }

  // 回调函数的包装
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange();
    }
  }

  //注册回调函数
  //如果没有parentSub,说明是根组件注册到store上
  //如果有,就注册到父组件的监听类上
  trySubscribe() {
    this.parentSub
      ? this.parentSub.addNestedSub(this.handleChangeWrapper)
      : this.store.subscribe(this.handleChangeWrapper);
  }
}

Subscription源码

对应改造

Provider

在我们使用 redux 的时候,Provider始终是我们的根组件,所以需要给Provider创建一个Subscription实例再通过context传递下去,线上代码

export const Provider = (props) => {
  const { store, children, context } = props;

  // 传给子组件的context{store,subscription}
  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();
    // 如果state发生改变,通知监听者
    if (previousState !== store.getState()) {
      subscription.onStateChange();
    }
  }, [contextValue, previousState, store]);

  const Context = context || ReactReduxContext;
  return <Context.Provider value={contextValue}>{children}</Context.Provider>;
};

Provider源码

Connect

在之前的版本中,connect 是直接注册到 store 上,那现在就应该注册在父级的subscription上,在自己更新完成之后,再去通知自己的子级做更新。

还有就是我们需要重写context中的subscription,因为当前组件拿到的subscription是属于它父级的,而当前组件的子级需要的subscription是当前组件创建的,我们需要重写context中的subscription,所以我们的connect返回的组件需要用Context.Provider包裹一下。线上代码

export const connect = (mapStateToProps, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {
  const { ...wrapperProps } = props;
  const context = useContext(ReactReduxContext);
  const { store, subscription: parentSub } = context; // 解构出store
  const subscription = new Subscription(store, parentSub); // 创建当前组件的subscription
  // 保存上一次的值
  const lastChildProps = useRef();
  //使用useReducer得到一个强制更新函数
  const [, forceComponentUpdateDispatch] = useReducer((count) => count + 1, 0);

  // 获取传递给组件的props
  const childPropsSelector = (store, wrapperProps) => {
    const state = store.getState();
    // 执行mapStateToProps和mapDispatchToProps
    const stateProps = mapStateToProps?.(state);
    const dispatchProps = mapDispatchToProps?.(store.dispatch);
    return Object.assign({}, stateProps, dispatchProps, wrapperProps);
  };

  //对比state,处理回调
  const compareStateForUpdate = () => {
    const newChildProps = childPropsSelector(store, wrapperProps);
    if (isEqual(newChildProps, lastChildProps.current)) return;
    lastChildProps.current = newChildProps;
    forceComponentUpdateDispatch();
    subscription.notifyNestedSubs();
  };
  const actualChildProps = childPropsSelector(store, wrapperProps);

  useLayoutEffect(() => {
    lastChildProps.current = actualChildProps;
  }, [actualChildProps]);

  // 使用subscription注册回调
  subscription.onStateChange = compareStateForUpdate;
  subscription.trySubscribe();

  //重写contextValue,把自己的subscription传递下去
  const overWriteContextValue = {
    ...context,
    subscription
  };

  return (
    <ReactReduxContext.Provider value={overWriteContextValue}>
      <WrappedComponent {...actualChildProps} />
    </ReactReduxContext.Provider>
  );
};

connect源码

总结

在本文中,提出了上一篇文章中connect实现的问题,由于 Redux 的引入使得 React 原本的数据流遭遇破坏。通过引入Subscription类实现发布订阅模式,来保证父父→子的一个更新顺序。数据发生改变时,从根组件开始通知自己的子组件,子组件通知其子组件,这样来保证更新顺序。