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

简单聊一聊 React, Redux 和 Context 的行为 #28

Open
hacker0limbo opened this issue Jun 7, 2022 · 0 comments
Open

简单聊一聊 React, Redux 和 Context 的行为 #28

hacker0limbo opened this issue Jun 7, 2022 · 0 comments
Labels
react react 笔记整理 redux redux 笔记整理

Comments

@hacker0limbo
Copy link
Owner

这篇文章主要参考了 Blogged Answers: React, Redux, and Context Behavior

关于 React Redux 和 Context 网上存在一些误解:

  • React-Redux 只是"对 React Context" 的包装
  • 你可以通过解构 context 里的属性来避免组件的重复渲染

上面说法说法均错误

其实这是一个 context 老生常谈的问题, 如果给 context 传入的是一个非原始类型, 比如数组或者对象, 那么当你的组件只订阅了部分对象属性, 即使该属性没有发生变化, 但如果其他属性发生变化你的组件仍旧会被迫重新渲染. 可以简单理解为没办法进行局部订阅, 除非你自己去做好性能优化.

举个例子:

function ProviderComponent() {
  const [contextValue, setContextValue] = useState({ a: 1, b: 2 });

  return (
    <MyContext.Provider value={contextValue}>
      <SomeLargeComponentTree />
    </MyContext.Provider>
  );
}

function ChildComponent() {
  const { a } = useContext(MyContext);
  return <div>{a}</div>;
}

如果 ProviderComponent 调用了 setContextValue({ a: 1, b: 3 }), ChildComponent 仍旧会被重新渲染, 即使他解构了对象并且只用到了 a 属性. 原因很简单, 一个新的对象引用被传递给了 provider, 所有的 consumer 都需要重新渲染. 事实上, 如果显示的强制调用一遍 <MyContext.Provider value={{a: 1, b: 2}}>, ChildComponent 仍旧会被重新渲染, 因为这是一个新的对象引用. 可以看做两个用 === 在进行严格比较. 所以理论上尽量不要给 Context 传递对象类型...

而对于 React-Redux, 虽然内部确实用到 context, 但他传递给 provider 的是 store 实例本身, 而非 store 内部的状态. 其基本实现可以看做如下:

function useSelector(selector) {
  const [, forceRender] = useReducer((counter) => counter + 1, 0);
  const { store } = useContext(ReactReduxContext);

  const selectedValueRef = useRef(selector(store.getState()));

  useLayoutEffect(() => {
    const unsubscribe = store.subscribe(() => {
      const storeState = store.getState();
      const latestSelectedValue = selector(storeState);

      if (latestSelectedValue !== selectedValueRef.current) {
        selectedValueRef.current = latestSelectedValue;
        forceRender();
      }
    });

    return unsubscribe;
  }, [store]);

  return selectedValueRef.current;
}

其基本原理也是相当清晰了. 通过订阅 Redux 的 store, 来获知是否有 action 被 dispatch 了, 然后通过 refuseLayoutEffect 来获取 store 里新旧值并进行比对来判断组件是否需要重新渲染. 注意这里还是进行的严格比较. 这也是 useSelectormapStateToProps 的区别. 虽然 react-redux 帮忙做了部分性能优化. 但是更加具体的还是需要自己来. 这里不展开.

注意由于通过 context 传递的是 store 的实例, 所以本质上 useLayoutEffect 不会触发多次渲染, 监听也只会监听一次.

关于 context 的行为可以参考: React issue #14110: Provide more ways to bail out of hooks.. 该 issue 很长, 这里只截取 Dan 在开头提到的两个点:

而对于什么时候使用 context, 在该 issue 下有人总结了一下:

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

中文翻译过来就是传给 context 的值一般是比较少会触发更新的, 比如 locale 或者 theme. context 更被常用的方式应该为注入一个服务, 而不是注入一个状态.

而对于 React-Redux v6, 作者曾尝试把 store state 作为 value 传递给 context. 但是最后证明这种方式存在很大问题. 具体细节可以参考: which is why we had to rewrite the internal implementation to use direct subscriptions again in React-Redux v7. 以及如果想要了解更多关于 React-Redux 工作原理的, 可以看另一篇博客: The History and Implementation of React-Redux

题外话

原文章的作者是 Mark Erikson, 是目前 Redux 和 Redux Toolkit 的维护者, 虽然相比于 Dan Abramov 可能没有那么出名. 但是他的博客质量还是非常高的, 他写了挺多关于 React 和 Redux 的文章, 文章都很长, 而且相当硬核. 不过可能由于过于硬核或者其他原因我是没找到国内的翻译版.

他的博客: https://blog.isquaredsoftware.com/

参考

@hacker0limbo hacker0limbo added redux redux 笔记整理 react react 笔记整理 labels Jun 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react react 笔记整理 redux redux 笔记整理
Projects
None yet
Development

No branches or pull requests

1 participant