精读《React Hooks 数据流》

1 引言

React Hooks 渐渐被国内前端团队所接受,但基于 Hooks 的数据流方案却还未固定,咱们有 “100 种” 相似的选择,却各有利弊,让人难以取舍。前端

本周笔者就深刻谈一谈对 Hooks 数据流的理解,相信读完文章后,能够从百花齐放的 Hooks 数据流方案中看到本质。react

2 精读

基于 React Hooks 谈数据流,咱们先从最不容易产生分歧的基础方案提及。git

单组件数据流

单组件最简单的数据流必定是 useStategithub

function App() {
  const [count, setCount] = useState();
}
复制代码

useState 在组件内用是毫无争议的,那么下个话题就必定是跨组件共享数据流了。redux

组件间共享数据流

跨组件最简单的方案就是 useContext缓存

const CountContext = createContext();

function App() {
  const [count, setCount] = useState();
  return (
    <CountContext.Provider value={{ count, setCount }}> <Child /> </CountContext.Provider> ); } function Child() { const { count } = useContext(CountContext); } 复制代码

用法都是官方 API,显然也是毫无争议的,但问题是数据与 UI 不解耦,这个问题 unstated-next 已经为你想好解决方案了。微信

数据流与组件解耦

unstated-next 能够帮你把上面例子中,定义在 App 中的数据单独出来,造成一个自定义数据管理 Hook:ide

import { createContainer } from "unstated-next";

function useCounter() {
  const [count, setCount] = useState();
  return { count, setCount };
}

const Counter = createContainer(useCounter);

function App() {
  return (
    <Counter.Provider> <Child /> </Counter.Provider> ); } function Child() { const { count } = Counter.useContainer(); } 复制代码

数据与 App 就解耦了,这下 Counter 不再和 App 绑定了,Counter 能够和其余组件绑定做用了。函数

这个时候性能问题就慢慢浮出了水面,首当其冲的就是 useState 没法合并更新的问题,咱们天然想到利用 useReducer 解决。性能

合并更新

useReducer 可让数据合并更新,这也是 React 官方 API,毫无争议:

import { createContainer } from "unstated-next";

function useCounter() {
  const [state, dispath] = useReducer(
    (state, action) => {
      switch (action.type) {
        case "setCount":
          return {
            ...state,
            count: action.setCount(state.count),
          };
        case "setFoo":
          return {
            ...state,
            foo: action.setFoo(state.foo),
          };
        default:
          return state;
      }
      return state;
    },
    { count: 0, foo: 0 }
  );

  return { ...state, dispatch };
}

const Counter = createContainer(useCounter);

function App() {
  return (
    <Counter.Provider> <Child /> </Counter.Provider> ); } function Child() { const { count } = Counter.useContainer(); } 复制代码

这下即使要同时更新 countfoo,咱们也能经过抽象成一个 reducer 的方式合并更新。

然而还有性能问题:

function ChildCount() {
  const { count } = Counter.useContainer();
}

function ChildFoo() {
  const { foo } = Counter.useContainer();
}
复制代码

更新 foo 时,ChildCountChildFoo 同时会执行,但 ChildCount 没用到 foo 呀?这个缘由是 Counter.useContainer 提供的数据流是一个引用总体,其子节点 foo 引用变化后会致使整个 Hook 从新执行,继而全部引用它的组件也会从新渲染。

此时咱们发现能够利用 Redux useSelector 实现按需更新。

按需更新

首先咱们利用 Redux 对数据流作一次改造:

import { createStore } from "redux";
import { Provider, useSelector } from "react-redux";

function reducer(state, action) {
  switch (action.type) {
    case "setCount":
      return {
        ...state,
        count: action.setCount(state.count),
      };
    case "setFoo":
      return {
        ...state,
        foo: action.setFoo(state.foo),
      };
    default:
      return state;
  }
  return state;
}

function App() {
  return (
    <Provider store={store}> <Child /> </Provider>
  );
}

function Child() {
  const { count } = useSelector(
    (state) => ({ count: state.count }),
    shallowEqual
  );
}
复制代码

useSelector 可让 Childcount 变化时才更新,而 foo 变化时不更新,这已经接近较为理想的性能目标了。

useSelector 的做用仅仅是计算结果不变化时阻止组件刷新,但并不能保证返回结果的引用不变化。

防止数据引用频繁变化

对于上面的场景,拿到 count 的引用是不变的,但对于其余场景就不必定了

举个例子:

function Child() {
  const user = useSelector((state) => ({ user: state.user }), shallowEqual);

  return <UserPage user={user} />; } 复制代码

假设 user 对象在每次数据流更新引用都会发生变化,那么 shallowEqual 天然是不起做用,那咱们换成 deepEqual深对比呢?结果是引用依然会变,只是重渲染不那么频繁了:

function Child() {
  const user = useSelector(
    (state) => ({ user: state.user }),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  // 但此处拿到的 user 引用仍是会变化

  return <UserPage user={user} />; } 复制代码

是否是以为在 deepEqual 的做用下,没有触发重渲染,user 的引用就不会变呢?答案是会变,由于 user 对象在每次数据流更新都会变,useSelectordeepEqual 做用下没有触发重渲染,但由于全局 reducer 隐去组件本身的重渲染依然会从新执行此函数,此时拿到的 user 引用会不断变化。

所以 useSelector deepEqual 必定要和 useDeepMemo 结合使用,才能保证 user 引用不会频繁改变:

function Child() {
  const user = useSelector(
    (state) => ({ user: state.user }),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 复制代码

固然这是比较极端的状况,只要看到 deepEqualuseSelector 同时做用了,就要问问本身其返回的值的引用会不会发生意外变化。

缓存查询函数

对于极限场景,即使控制了重渲染次数与返回结果的引用最大程度不变,仍是可能存在性能问题,这最后一块性能问题就处在查询函数上。

上面的例子中,查询函数比较简单,但若是查询函数很是复杂就不同了:

function Child() {
  const user = useSelector(
    (state) => ({ user: verySlowFunction(state.user) }),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 复制代码

咱们假设 verySlowFunction 要遍历画布中 1000 个组件的 n 3 次方次,那组件的重渲染时间消耗与查询时间相比彻底不值一提,咱们须要考虑缓存查询函数。

一种方式是利用 reselect 根据参数引用进行缓存。

想象一下,若是 state.user 的引用不频繁变化,但 verySlowFunction 很是慢,理想状况是 state.user 引用变化后才从新执行 verySlowFunction,但上面的例子中,useSelector 并不知道还能这么优化,只能傻傻的每次渲染重复执行 verySlowFunction,哪怕 state.user 没有变。

此时咱们要告诉引用,state.user 是否变化才是从新执行的关键:

import { createSelector } from "reselect";

const userSelector = createSelector(
  (state) => state.user,
  (user) => verySlowFunction(user)
);

function Child() {
  const user = useSelector(
    (state) => userSelector(state),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 复制代码

在上面的例子中,经过 createSelector 建立的 userSelector 会一层层进行缓存,当第一个参数返回的 state.user 引用不变时,会直接返回上一次执行结果,直到其应用变化了才会继续往下执行。

这也说明了函数式保持幂等的重要性,若是 verySlowFunction 不是严格幂等的,这种缓存也没法实施。

看上去很美好,然而实战中你可能发现没有那么美好,由于上面的例子都创建在 Selector 彻底不依赖外部变量

结合外部变量的缓存查询

若是咱们要查询的用户来自于不一样地区,须要传递 areaId 加以识别,那么能够拆分为两个 Selector 函数:

import { createSelector } from "reselect";

const areaSelector = (state, props) => state.areas[props.areaId].user;

const userSelector = createSelector(areaSelector, (user) =>
  verySlowFunction(user)
);

function Child() {
  const user = useSelector(
    (state) => userSelector(state, { areaId: 1 }),
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 复制代码

因此为了避免在组件函数内调用 createSelector,咱们须要尽量将用到外部变量的地方抽象成一个通用 Selector,并做为 createSelector 的一个先手环节。

userSelector 提供给多个组件使用时缓存会失效,缘由是咱们只建立了一个 Selector 实例,所以这个函数还须要再包装一层高阶形态:

import { createSelector } from "reselect";

const userSelector = () =>
  createSelector(areaSelector, (user) => verySlowFunction(user));

function Child() {
  const customSelector = useMemo(userSelector, []);

  const user = useSelector(
    (state) => customSelector(state, { areaId: 1 }),
    deepEqual
  );
}
复制代码

因此对于外部变量结合的环节,还须要 useMemouseSelector 结合使用,useMemo 处理外部变量依赖的引用缓存,useSelector 处理 Store 相关引用缓存。

3 总结

基于 Hooks 的数据流方案不能算完美,我在写做这篇文章时就感受到这种方案属于 “浅入深出”,简单场景还容易理解,随着场景逐步复杂,方案也变得愈来愈复杂。

但这种 Immutable 的数据流管理思路给了开发者很是自由的缓存控制能力,只要透彻理解上述概念,就能够开发出很是 “符合预期” 的数据缓存管理模型,只要精心维护,一切就变得很是有秩序。

讨论地址是:精读《React Hooks 数据流》 · Issue #242 · dt-fe/weekly

若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

相关文章
相关标签/搜索