自定义 hook 简单实现 React 状态管理

·  阅读 1214

背景

在我们开发前端需求时,会遇到多个组件内部状态同步的问题,这些组件往往在 DOM 结构上相距甚远。以网页中常见的“换肤功能”为例,ThemeSwitch 组件在更改内部主题色状态 color 时,希望实时更新其他组件的主题色,实现动态主题切换实时预览功能:

为了实现类似的功能,大家可能会使用 Redux 之类的状态管理库。但是有两个问题值得我们思考:

  1. 我只是想同步个状态,能简单实现不引入第三方类库么?
  2. 状态管理类库内部是如何实现的,原理是什么?

问题分析

分析问题的思路有很多,大致可以分为两种:

  • 0 - 1 :先把代码跑起来,一步步遇到问题,解决问题;适合解题路径较清晰问题;
  • 1 - 0 :从结果出发,分析要想达到该结果的前置条件,追本溯源;适合拆解复杂问题;

这里我们采用 1 - 0 的方法来分析:

1. 通知同步

要想达到状态同步,当主题切换组件 ThemeSwitch 改变内部主题状态,执行 setColor(newColor)时,我们只要 通知 其他组件执行同样 setColor(newColor) 操作更新自己的状态就行,这样就达到了状态同步目的:

2. 如何通知?

通知其实就是执行各个组件内部的 setColor 方法,这里就需要将 setColor 收集 起来,然后在需要同步的时候依次调用,结合上步的 通知,我们很容易想到这是一个 发布-订阅 模型。各组件在声明内部状态 useState() 时,是个很好的订阅时机,我们可以自定义一个 hooks useColorState 劫持该操作, 作为 Controller, 维护 订阅-发布 功能:

解决

通过以上的分析,我们理清了思路:实现一个自定义的 hook 去拦截各子组件内部状态的定义,以便绑定订阅关系,留下操作空间;自定义 hook 需要实现发布订阅功能,在状态变更时通知各组件;

1. 自定义 hook

export default function useColorState() {
  const [color, setColor] = useState();
  return [color, setColor];
}
复制代码

2. 「发布-订阅」模型

class ColorChanger {
  constructor() {
    this.color = 'orange';
    this.callBacks = [];
  }

  subscribe(cb) {
    this.callBacks.push(cb);
  }

  publish() {
    this.callBacks.forEach((cb) => cb(this.color));
  }
}
复制代码

3. 订阅

完成订阅和内部状态初始化:

let colorChanger;

function useColorState() {
  // ...
  useEffect(() => {
    colorChanger = colorChanger || new ColorChanger();
    colorChanger.subscribe(setColor);
    setColor(colorChanger.color);
  }, []);
  // ...
}
复制代码

4. 发布

这里 hook 返回只是更新自己状态的 setColor, 我们需要通知其他组件:

function useColorState() {
  // ...
  const changeColor = (val) => {
    colorChanger.setColor(val);
  };
  return [color, changeColor];
}
复制代码
class ColorChanger {
  // ...
  setColor(color) {
    this.color = color;
    this.publish();
  }
}
复制代码

5. 取消订阅

当组件卸载时,我们需要取消订阅,将该组件的 setColor 从 colorChanger.callbacks 中删除即可:

class ColorChanger {
  // ...
  unsubscribe(cb) {
    this.callBacks.splice(this.callBacks.indexOf(cb), 1);
  }
}

function useColorState() {
  useEffect(() => {
    // ...
    return () => {
      colorChanger.unsubscribe(setColor);
    };
  }, []);
}
复制代码

优化

到这里,通过自定义 hook 和 发布-订阅模式,我们实现了一个简单的状态同步功能,但是依然有一些问题在 debug 时被发现了:

1. 小优化

在执行 setColor(sameColor) 时,依然触发了发布,而发布是一个高成本操作,所以这里需要优化:

setColor(color) {
    if(this.color === color) return;
    // ...
}
复制代码

2. Reducer

Redux 一般都是拥有复杂深度的 Object,我们这里只是维护了一个 string 类型的简单状态 color,那 Redux 要怎么对比新老状态是否变更了呢? 答案是 Reducer:

上图就是一个 reducer,很形象:通过状态比对过滤后返回真正需要变更的状态。以下是一个 Reducer 的定义:

const newState = reducer(oldState, action);
复制代码

所以我们也改造下 useColorState:

function colorReducer(state = { color: 'orange' }, action) {
  return action.color === state.color ? state : { color: action.color };
}

function useColorState(colorReducer) {
  colorChanger = colorChanger || new ColorChanger(colorReducer);
  // ...
}

class ColorChanger {
  setColor(action) {
    const newState = this.colorReducer(this.state, action);
    // ...
  }
}
复制代码

Redux Reducer 设计避免了每次需要深度遍历比较新旧状态,而是通过 reducer 判断没更新就返回老对象。由于我们的场景比较简单,这里直接使用 === 比较对象。当然还有很可以针对性再进一步地优化: 比如 changeColor 应该使用 useCallback 包裹防止 setColor 作为 props 传递产生的不必要的渲染、比如发布的时候应该做个队列确保本轮通知完成再进行下轮状态变更。

总结

本文探讨了 web 开发过程中常见的组件状态同步问题,并介绍一种简单实用的解决方案。读者在阅读完本文后,可以尝试阅读 React Context 实现,原理与此类似。在阅读源码的过程中,你会发现第三方库尽管看起来代码量很多,技术原理可能并没有想象中那么复杂。在掌握了分析问题的能力与相关的知识后,阅读源码也会更加顺畅。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改