面试官:你说你做过组件库,肯定了解过复杂组件状态管理的useSyncExternalStore吧?我:😭

6,260 阅读3分钟

平时我们进行业务开发可能不会遇到这种状况。

但是当我们需要封装一个比较复杂一点的组件,比如 Tree、Table 等。因为基本不用zustand或者react-redux。 我们往往要在组件内部自己维护一个管理者来帮我们处理组件内部的数据。 但是处理数据其实 react 不会感知到也不会重渲染,这里有一个套路。

那么当你学会了这个套路之后,开发组件就能变的更加游刃有余

🤖 例子:假设我们现在要封装一个 Tree 组件

🚀 第一步:

创建一个管理数据的管理者,把treeDataTreeManager保存

class TreeManager {
  constructor(treeData) {
    this.treeData = treeData;
  }
}

treeManager存在 Provider里备用,通过useContext(TreeManagerContext)在所有的子组件使用

const treeData = [{ name: 'A' }, { name: 'B' }];
function Tree() {
  const [treeManager, _] = useState(() => new TreeManager(treeData));
  return (
    <TreeManagerContext.Provider value={treeManager}>
        子组件都能拿到treeManager
    </TreeManagerContext.Provider>
  );
}

🚀 第二步:

创建一个row组件

通过useTreeNode,拿到TreeManager.treeData数组里面对应的项,展示name属性

function Row(props) {
  const { index } = props;
  const treeNode = useTreeNode(index);
  return <div>{treeNode.name}</div>;
}

🚀 第三步:

我们现在写死这个树只有两个<Row/>分别对应上面treeManager.treeData[{ name: 'A' }, { name: 'B' }] 数组的两项

function Tree() {
  const [treeManager, _] = useState(() => new TreeManager(treeData));
  return (
    <TreeManagerContext.Provider value={treeManager}>
  +    <Row index={0} />
  +    <Row index={1} />
    </TreeManagerContext.Provider>
  );
}

问题来了,现在我这么做,调用 treeManager.edit() 修改了 treeData 数组第一项的 nameA 改为 许泽川

先别看代码,我们要的效果是不是这样的,修改到的那一项对应的 Row 组件重渲染,没修改到的 Row 组件不重渲染,也就是说

  1. <Row index={0} /> 重渲染,从A变成许泽川

  2. <Row index={1} />不重渲染还是显示 B

class TreeManager {
  constructor(treeData) {
    this.treeData = treeData
  }
+ edit() {
+  this.treeData[0].name = '许泽川'
+ }
}
function Tree() {
  const [treeManager, _] = useState(() => new TreeManager(treeData));
 +  treeManager.edit()
  return (
    <TreeManagerContext.Provider value={treeManager}>
      <Row index={0} />
      <Row index={1} />
    </TreeManagerContext.Provider>
  );
}

🤖 怎么做到这种按需渲染呢?

1. 看一下我的 useTreeNode 怎么实现的

function useTreeNode(index) {
  const treeNodeManager = useContext(TreeManagerContext);
  const treeNode = useSyncExternalStore(
    treeNodeManager.subscribe,
    () => treeNodeManager.data[index],
    () => treeNodeManager.data[index]
  );
  return treeNode;
}

下面这段话就是本篇重点,看完大概 useSyncExternalStore 就了解了

  1. 里面用到了 useSyncExternalStore,他的原理简单来说就是,useSyncExternalStore 内部会通过你传入的这个函数 () => treeNodeManager.data[index] 拿到值给你,就是这么简单
  2. 但是 useSyncExternalStore 额外搞了一点小动作,他通过你提供的管理库的订阅函数 treeNodeManager.subscribe,订阅一个回调函数给 treeNodeManager
  3. 只要你触发了这个回调,他就会重新调用 () => treeNodeManager.data[index] 拿到最新值,跟上一次拿到的值做对比,如果变了,就会让用到这个 useSyncExternalStore 的函数组件重渲染

你看看,他聪明的,会做对比,变化了的项才重渲染,这不就是 按需渲染 吗?

2. 那怎么触发回调,什么时候触发回调呢?

所以我们需要稍微修改一下 treeNodeManager,简简单单加一个观察者模式

修改完数据后,都调用一下这个回调

一切这不就齐活了,懂了吗您?

class TreeManager {
  constructor(treeData) {
    this.treeData = treeData
  }
  edit() {
    this.treeData[0].name = '许泽川'
    this.listeners.forEach(listener => listener())
  }

  listeners = []
  subscribe(callback) {
    this.listeners.push(callback)
  }
}

🤖 相关

面试官:让你实现一个高性能 Tree 组件磕磕绊绊的,你好意思干了三年前端?我:😭

面试官:你说你做过组件库,肯定了解过复杂组件状态管理的useSyncExternalStore吧?我:😭

面试官:你跟我说 setState 是同步的,它不是异步的吗?背错面试题了吧你!我:😭

面试官:你说你开发过组件库,那你怎么会不知道受控组件?面试就到这里吧。我:😭

版权归许泽川所有

如需转载,请提前询问本人的许可