zh-rocco / fe-notes

:memo: 前端笔记

Home Page:https://zh-rocco.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

【React】Hooks 从入门到放弃

zh-rocco opened this issue · comments

commented

【React】Hooks 从入门到放弃

HookReact 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

背景

在 Hooks 出现之前, 函数组件对比类组件(class)形式有很多局限, 例如:

  • 不能使用 state、ref 等属性, 只能通过函数传参的方式使用 props
  • 没有生命周期钩子

对比类组件

类组件缺点:

  • 在复杂组件中, 耦合的逻辑代码很难分离
  • 组件间逻辑复用困难
  • 监听清理和资源释放问题, 同一资源的生成和销毁逻辑不在一起, 被分割在 mountunmount 钩子中
  • class 学习成本, 比如 this 指向问题

Hooks 缺点

  • 依赖传染性, 这导致了开发复杂性的提高、可维护性的降低
  • 缓存雪崩, 这导致运行性能的降低
  • 异步任务下无法批量更新, 闭包问题

运行过程对比

类组件 函数组件
第一次执行 class 实例化 -> 执行 render 函数 执行函数 -> 初始化 useState
后续执行 重新执行 render 函数 重新执行函数 -> 从 useState 中获取状态

Hooks 注意事项

  • 只能在函数组件或 custom Hook 的最外层调用 Hooks, 不要在循环、条件判断、事件处理器中调用
  • 不要在类组件和其他 JavaScript 函数中调用
  • 不要在 useMemo, useReducer, useEffect 内调用 Hooks

闭包问题

一些好的实践

state 拆分与分组

粒度过细

const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);

粒度过粗

const [state, setState] = useState({
  width: 100,
  height: 100,
  left: 0,
  top: 0,
});

Good

const usePosition = () => {
  const [position, setPosition] = useState({ left: 0, top: 0 });

  useEffect(() => {
    // ...
  }, []);

  return [position, setPosition];
};

const Box = () => {
  const [position, setPosition] = usePosition();
  const [size, setSize] = useState({ width: 100, height: 100 });
  // ...
};

使用 useState 时, 我们需要考虑状态拆分的粒度问题, 如果粒度过细, 代码就会变得冗余, 并且在更新 state 时会频繁触发函数组件的 re-render, 如果粒度过粗, 代码的可复用性就会降低。

建议

  • 将完全不相关的 state 拆分为多组 state
    • 比如 sizeposition
  • 如果某些 state 是相互关联的, 或者需要一起发生改变, 就可以把它们合并为一组 state
    • 比如 lefttop

dependency 数量优化

如果发现依赖数组依赖过多, 我们就需要重新审视自己的代码

  • 依赖数组依赖的值最好不要超过 3 个, 否则会导致代码会难以维护
  • 如果发现依赖数组依赖的值过多, 我们应该采取一些方法来减少它
    • 去掉不必要的依赖
    • Hook 拆分为更小的单元, 每个 Hook 依赖于各自的依赖数组
    • 通过合并相关的 state, 将多个依赖值聚合为一个
    • 通过 setState 回调函数获取最新的 state, 以减少外部依赖
    • 通过 ref 来读取可变变量的值, 不过需要注意控制修改它的途径

该不该使用 useMemo

对于这个问题, 有的人从来没有思考过, 有的人甚至不觉得这是个问题, 因为我们在网上经常见到的做法是, 那就是不管什么情况, 只要用 useMemo 或者 useCallback 简单的『包裹一下』, 似乎就能使应用远离性能的问题, 但真的是这样吗?有的时候 useMemo 没有任何作用, 甚至还会影响应用的性能

useMemo 适用的场景

  • 有些计算开销很大, 我们就需要『记住』它的返回值, 避免每次 render 都去重新计算
    • 比如 cloneDeep 一个很大并且层级很深的数据
  • 由于值的引用发生变化, 导致下游组件重新渲染, 我们也需要『记住』这个值
    • 对于组件内部用到的 ObjectArrayFunction 等, 如果用在了其他 Hook 的依赖数组中, 或者作为 props 传递给了下游组件, 应该使用 useMemo
    • 自定义 Hook 中暴露出来的 ObjectArrayFunction 等, 都应该使用 useMemo, 以确保当值相同时, 引用不发生变化

有一个误区就是对创建函数开销的评估, 有的人觉得在 render 中创建函数可能会开销比较大, 为了避免函数多次创建, 使用了 useMemo 或者 useCallback, 但是对于现代浏览器来说, 创建函数的成本微乎其微

如果因为 prop 的值相同而引用不同, 从而导致子组件发生 re-render, 不一定会造成性能问题, 因为 Virtual DOM re-render 并不等同于 DOM re-render, 但是当子组件特别大时, Virtual DOMDiff 开销也很大, 因此还是应该尽量避免子组件 re-render

最后的最后, 我们再来将上面涉及到的内容整体的回顾一下

  • 将完全不相关的 state 拆分为多组 state
  • 如果某些 state 是相互关联的, 或者需要一起发生改变, 就可以把它们合并为一组 state
  • 依赖数组的值最好不要过多, 如果发现依赖数组依赖的值过多, 我们应该采取一些方法来减少它
    • 去掉不必要的依赖
    • 将 Hook 拆分为更小的单元, 每个 Hook 依赖于各自的依赖数组
    • 通过合并相关的 state, 将多个依赖值聚合为一个
    • 通过 setState 回调函数获取最新的 state, 以减少外部依赖
    • 通过 ref 来读取可变变量的值, 不过需要注意控制修改它的途径
  • 应该使用 useMemo 的场景
    • 保持引用相等
    • 成本很高的计算
  • 无需使用 useMemo 的场景
    • 如果返回的值是原始值, 一般不需要使用 useMemo
    • 仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件), 且没有用到其他 Hook 的依赖数组中, 一般不需要使用 useMemo
  • 若 Hook 类型相同, 且依赖数组一致时, 应该合并成一个 Hook
  • 自定义 Hook 的返回值可以使用 Tuple 类型, 更易于在外部重命名, 如果返回的值过多, 则不建议使用
  • ref 不要直接暴露给外部使用, 而是提供一个修改值的方法
  • 在使用 useMemo 或者 useCallback 时, 可以借助 ref 或者 setState callback, 确保返回的函数只创建一次, 也就是说, 函数不会根据依赖数组的变化而二次创建

强大的 useEffect

摆脱类组件的思维模式

当使用生命周期钩子时,我们需要手动去判断哪些数据(dataRange)发生了变化,然后更新到对应的数据(data)。

而在 Hooks 的使用中,我们只需关注哪些值(dataRange)需要进行同步。

const Chart = ({ dateRange }) => {
  const [data, setData] = useState();

  useEffect(() => {
    const newData = getDataWithinRange(dateRange);
    setData(newData);
  }, [dateRange]);

  return <svg className="Chart" />;
};
  1. 使用 useEffect 进行数据的处理;
  2. 存储变量到 state;
  3. 在 JSX 中引用 state。

我们不需要使用 state ,那是类组件的开发模式,因为在类组件中,render 函数和生命周期钩子并不是在同一个函数作用域下执行,所以需要 state 进行中间的存储,同时执行的 setState 让 render 函数再次执行,借此获取最新的 state。

而在函数式组件中我们有时根本不会需要用到 state 这样的状态存储,我们仅仅是想使用。

const Chart = ({ dateRange }) => {
  const data = useMemo(() => getDataWithinRange(dateRange), [dateRange]);

  return <svg className="Chart" />;
};

因为函数组件中 render 和生命周期钩子在同一个函数作用域中,这也就意味着不再需要 state 作中间数据桥梁,我们可以直接在函数执行时获取到处理的数据,然后在 return 的 JSX 中使用,不必需要每次使用属性都要在 state 中声明和创建了,不再需要重新渲染执行一次函数(setData)了,所以我们去除掉了 useState。这样,我就减少了一个 state 的声明以及一次重新渲染。

我们把变量定义在函数里面,而不是定义在 state 中,这是类组件由于其结构和作用域上与函数组件相比的不足,是函数组件的优越性。

避坑指南

  • 需要注意的是, 我们在使用 ref 的过程当中要特别小心, 因为它可以随意赋值, 所以一定要控制好修改它的方法, 特别是一些底层模块, 在封装的时候千万不要直接暴露 ref, 而是提供一些修改它的方法

感受一下 Hook 的优势

类组件:

class Chart extends Component {
  state = {
    data: null,
    dimensions: null,
    xScale: null,
    yScale: null,
  };
  componentDidMount() {
    const newData = getDataWithinRange(this.props.dateRange);
    this.setState({ data: newData });
    this.setState({ dimensions: getDimensions() });
    this.setState({ xScale: getXScale() });
    this.setState({ yScale: getYScale() });
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.dateRange != this.props.dateRange) {
      const newData = getDataWithinRange(this.props.dateRange);
      this.setState({ data: newData });
    }
    if (prevProps.margins != this.props.margins) {
      this.setState({ dimensions: getDimensions() });
    }
    if (prevState.data != this.state.data) {
      this.setState({ xScale: getXScale() });
      this.setState({ yScale: getYScale() });
    }
  }
  render() {
    return <svg className="Chart" />;
  }
}

Hooks:

const Chart = ({ dateRange, margins }) => {
  const data = useMemo(() => getDataWithinRange(dateRange), [dateRange]);
  const dimensions = useMemo(getDimensions, [margins]);
  const xScale = useMemo(getXScale, [data]);
  const yScale = useMemo(getYScale, [data]);
  return <svg className="Chart" />;
};

参考