Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

再谈connect设计 #5

Open
LuckyFBB opened this issue Apr 3, 2022 · 0 comments
Open

再谈connect设计 #5

LuckyFBB opened this issue Apr 3, 2022 · 0 comments
Assignees
Labels

Comments

@LuckyFBB
Copy link
Owner

LuckyFBB commented Apr 3, 2022

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

回顾以及引入问题

在之前的设计中,我们使用 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类实现发布订阅模式,来保证父父→子的一个更新顺序。数据发生改变时,从根组件开始通知自己的子组件,子组件通知其子组件,这样来保证更新顺序。

@LuckyFBB LuckyFBB added the Redux label Apr 3, 2022
@LuckyFBB LuckyFBB self-assigned this Apr 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant