ArthurWangCN / notepad

reading notepad
0 stars 2 forks source link

React hooks #53

Open ArthurWangCN opened 4 months ago

ArthurWangCN commented 4 months ago

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

ArthurWangCN commented 4 months ago

和类组件对比有什么优点

优点:

  1. 更简洁: 相比于传统的 class 组件, 使用 Hooks 可以将组件的逻辑拆分成更小, 这使得组件代码更加简洁、易读、好维护
  2. 易上手: 使用 Hooks 你可以在函数组件中使用状态和其他 React 特性, 无需编写 class 从而避免了繁琐的 class 组件的声明和继承、同时也无需考虑 this 指向等问题
  3. 逻辑复用: 自定义 Hooks 允许将组件之间的状态逻辑进行抽离, 作为一个独立的可组合和可共享单元, 从而减少了重复代码的出现
  4. 更好的可测试性: 通过 Hooks 可以将组件渲染、和业务逻辑分离进行分离, 使得组件的测试变得更加容易。可以针对每个 Hook 编写单独的测试,确保其正确性, 同时保持组件测试的简洁性。
  5. 灵活性: Hooks 的设计允许你在组件内部使用多个不同的 Hook, 这使得你可以在一个函数组件中使用各种各样的特性, 而不必担心组件层次的嵌套和复杂性
  6. 有助于代码拆分: 使用 Hooks 可以更容易地拆分组件, 将组件的不同部分拆分成更小的逻辑单元,有助于更好地组织和管理代码。
  7. 类组件在业务不断扩展的情况下, 容易变得臃肿难以维护, 往往相关的业务被拆分到多个生命周期里, 或者一个生命周期中存在多个不相关的业务, 而 Hook 的出现, 可以将业务拆分为更小的函数, 对业务逻辑进行更为细腻的控制, 使得组件更容易理解、维护
  8. 可以把状态分开管理, 逻辑上更清晰了, 更方便维护了
  9. 补充: 类组件中如果需要复用状态逻辑, 只能通过高阶组件来实现, 没有 hooks 简洁, 而且还多了一层组件嵌套

缺点:

  1. 陡峭的学习曲线: 对于那些熟悉传统 class 组件的开发者来说, 学习 Hooks 可能需要一些时间。Hooks 改变了组件的编写方式, 并且需要理解如何正确地使用 useState、useEffect、useContext 等钩子函数
  2. 使用规则: Hooks 有一些使用规则, 例如在条件语句中不可使用, 或者只能在函数组件的最顶层使用。违反这些规则可能导致 bug 和意想不到的行为。
  3. 性能问题: 尽管 Hooks 通常可以优化组件逻辑, 但不正确地使用它们可能导致性能问题。比如, 在 useEffect 中没有正确处理依赖项数组可能会导致不必要的重复执行。

怎么避免 hooks 的常见问题:

  1. 不要在 useEffect 里面写太多的依赖项, 划分这些依赖项成多个单一功能的 useEffect 其实这点是遵循了软件设计的 单一职责模式
  2. 拆分组件, 细化组件的粒度, 复杂业务场景中使用 hooks 应尽可能地细分组件, 使得组件的功能尽可能单一, 这样的 hooks 组件更好维护
  3. 能通过事件触发数据更新, 就尽量通过事件方式去实现, 尽量避免在 useEffect 中依赖 A 状态然后去修改 B 状态
ArthurWangCN commented 4 months ago

常用的几个 Hooks

ArthurWangCN commented 4 months ago

useEffect、useLayoutEffect、useInsertionEffect 之间的区别

  1. useInsertionEffect: 应该是 DOM 变更之前执行
  2. useLayoutEffect: DOM 已经按照 VDOM 更新了, 此时 DOM 已经在内存中更新了, 但是还没有更新到屏幕上
  3. useEffect: 则是浏览器完成渲染之后执行
  4. 所以三者执行顺序: useInsertionEffect(DOM 变更前)、useLayoutEffect(DOM 变更后)、useEffect
  5. useLayoutEffect 与 useEffect 基本相同, 但它会在所有的 DOM 变更之后 同步 调用, 一般可以使用它来读取 DOM 布局并同步触发重渲染, 为了避免阻塞视觉更新, 我们需要尽可能使用标准的 useEffect
  6. useEffect 和 useLayoutEffect 都可用于模拟 componentDidUpdate componentDidMount
  7. 当父子组件都用到 useEffect 时, 子组件中的会比父组件中的先触发
ArthurWangCN commented 4 months ago

React.memo

在类组件的时代时代, 为了性能优化我们经常会选择使用 PureComponent, 组件每次默认会对 props 进行一次 浅比较, 只有当 props 发生变更, 才会触发 render

class MyComponent extends PureComponent {
  render () {}
}

当然在类组件中, 我们除了使用 PureComponent 还可以在 shouldComponentUpdate 生命周期中, 对 props 进行比较, 进行更深层次的控制;

补充: shouldComponentUpdate 当收到新的 props 或 state 时, 在渲染之前都会被调用 这里的比较可以是浅比较、也可以是深比较, 主要看代码实现 当 shouldComponentUpdate 返回为 true 的时候, 当前组件进行 render, 如果返回的是 false 则不进行 render

class MyComponent extends Component {
  shouldComponentUpdate(){
    if (需要 Render) {
      // 会进行渲染
      return true
    }
    // 不会进行渲染
    return false
  }
  render () {}
}

在函数组件中, 我们是无法使用上面两种方式来限制 render 的, 但是 React 贴心的提供了 React.memo 这个 HOC(高阶组件), 它的作用和 PureComponent 很相似, 只是它是专门为函数组件设计的

// 组件
function MyComponent(props) {}

// 比较方法
function areEqual(prevProps, nextProps) {
  if (需要 Render) {
    // 会进行渲染
    return false
  }

  // 不会进行渲染
  return true
}

export default React.memo(MyComponent, areEqual);

作用: 性能优化, 如果本组件中的数据没有发⽣变化, 阻⽌组件更新, 类似类组件中的 PureComponent 和 shouldComponentUpdate


总结: React.memo 是一个高阶组件,用于优化 React 函数组件的性能。它通过浅层比较(shallow comparison)来决定是否重新渲染组件,从而避免不必要的渲染,提升应用性能。 将一个函数组件传递给 React.memo,它会返回一个新组件,该组件在 props 没有变化时不会重新渲染。

ArthurWangCN commented 4 months ago

使用时需要注意什么

  1. 遵守 Hooks 使用规则: Hooks 只能在函数组件的顶层使用, 或者在自定义 hooks 中使用, 不能在循环、条件或嵌套函数中使用 hooks
  2. 依赖数组: 在使用 useEffect 或 useCallback 等 hooks 时, 务必提供依赖数组作为第二个参数。忽略或者错误的依赖数组可能导致意外行为, 比如过度重新渲染或内存泄漏
  3. 避免无限循环: 在使用 useEffect 时要小心无限循环, 确保依赖数组中有正确的依赖项, 并且 effect 的逻辑不会触发不必要的重新渲染
  4. 状态不可变性: 避免直接修改状态对象, 也不要试图通过 push、pop、splice 等直接更改数组
  5. 单一职责 组件、useEffects
  6. 尽量避免通过 useEffect 来处理 actions: useEffect 监听某个状态 A, 内部又去修改 A, 这样就容易造成死循环
  7. 如果某个数据的变更不需要触发 render, 或者该数据没有在 jsx 中被使用, 那么就不要使用 useState 改用 useRef 进行记录
ArthurWangCN commented 4 months ago

为什么 hooks 不能写在循环或者条件判断语句里?

Hooks 只能在函数组件的顶层使用, 或者在自定义 hooks 中使用, 不能在循环、条件或嵌套函数中使用 hooks

原因: React 需要利用 调用顺序 来正确更新相应的状态, 以及调用相应的钩子函数, 一旦在循环或条件分支语句中调用 Hooks, 就容易导致调用顺序的不一致性, 从而产生难以预料到的后果

这里拿 useState 来举例:

  1. hooks 为了在函数组件中引入状态, 维护了一个有序表
  2. 首次执行时会将每个 useState 的初始值, 依次 存到有序表里
  3. 每次更新也都会按照 索引 修改指定位置的值
  4. 每次 render 会将对应 索引 的值作为状态返回
  5. 那么试想下, 如果我们将 useState 写在判断条件下, 可能会导致 useState 不执行, 那么这个有序列表就会出现混乱
export default () => {
  const [name, setName] = useState('1');

  if (!name) {
    return null;
  }

  const [age, setAge] = useState();

  const handler = useCallback(() => {
    setName(null);
  }, []);

  return (
    <div onClick={handler}>
      点击我会报错
    </div>
  );
};

总结:

hooks 是将 state 原子化, 使用类似索引的方式来记录状态值, 当连续创建状态 A B, 就会有索引 0 对应着 A, 索引 1 对应这 B, 如果使用在循环、条件、嵌套函数内使用 Hook 就很容易造成索引错乱

ArthurWangCN commented 4 months ago

为什么 useState 返回的是一个数组?

useState 要返回两个值, 一个是当前状态, 另一个则是修改状态的方法, 那么这里它就有两种方式可以返回这两个值: 数组、对象

那么问题就回到, 数组和对象解构赋值的区别了:

数组的元素是按次序排列的, 数组解构时变量的取值由数组元素的位置决定, 变量名可以任意命名, 如下:

const [name, setName] = useState()
const [age, setAge] = useState()

对象的属性没有次序, 解构时变量名必须与属性同名才能取到正确的值, 假设 useState 返回的是一个对象, 那么就得这么使用:

const { state: name, setState: setName } = useState()
const { state: age, setState: setAge} = useState()

上面例子可以得出结果, useState 返回数组相比于对象会更灵活、解构起来也会更简洁、方便。

当然最终 useState 返回的是啥, 还是由具体实现决定, 如果 useState 返回的是对象, 也不是不行

ArthurWangCN commented 4 months ago

简单实现hooks

// 一、实现useState
const { render } = require("react-dom");
let memoriedStates = [];
let lastIndex = 0;
function useState(initialState) {
  memoriedStates[lastIndex] = memoriedStates[lastIndex] || initialState;
  function setState(newState) {
    memoriedStates[lastIndex] = newState;
    // 状态更新完毕,调用render函数。重新更新视图
    render();
  }
  // 返回最新状态和更新函数,注意 index 要前进
  return [memoriedStates[lastIndex++], setState];
}

// 二、实现useEffect
let lastDendencies; // 存放依赖项的数组
function useEffect(callback, dependencies) {
  if (lastDendencies) {
    // 判断传入的依赖项是不是都没有变化,只要有以一项改变,就需要执行callback
    const isChange = dependencies && dependencies.some((dep, index) => dep !== lastDendencies[index]);
    if (isChange) {
      // 一开始没有值,需要更新一次(相当于componentDidMount)
      typeof callback === 'function' && callback();
      // 更新依赖项
      lastDendencies = dependencies;
    }
  } else {
    // 一开始没有值,需要更新一次(相当于componentDidMount)
    typeof callback === 'function' && callback();
    // 更新依赖项
    lastDendencies = dependencies;
  }
}

// 三、实现useCallback
let lastCallback; // 最新的回调函数
let lastCallbackDependencies = []; // 回调函数的依赖项
function useCallback(callback, dependencies = []) {
  if (lastCallback) {
    const isChange = dependencies && dependencies.some((dep, index) = dep !== lastCallbackDependencies[index]);
    if (isChange) {
      // 只要有一个依赖项改变了,就更新回调(重新创建)
      lastCallback = callback;
      lastCallbackDependencies = dependencies;
    }
  } else {
    lastCallback = callback;
    lastCallbackDependencies = dependencies;
  }
  // 最后需要返回最新的函数
  return lastCallback;
}

// 四、实现useRef
let lastRef;
function useRef(initialValue = null){

  lastRef = lastRef != undefined ? lastRef : initialValue;
  // 本质上就是返回一个对象,对象种有一个current属性,值为初始化传入的值,如果没有传入初始值,则默认为null
  return {
    current: lastRef
  }
}

// 五、实现useContext
function useContext(context){
  // 很简单,就是返回context的_currentValue值
  return context._currentValue;
}

// 六、实现useReducer
let lastState;
function useReducer(reducer, initialState){
  lastState = lastState !== undefined ? lastState : initialState;
  // dispatch一个action,内部就是自动调用reducer来计算新的值返回
  function dispatch(action){
    lastState = reducer(lastState, action);
    // 更新完毕后,需要重新渲染视图
    render();
  }
  // 最后返回一个的状态值和派发action的方法
  return [lastState, dispatch];
}
ArthurWangCN commented 4 months ago

useCallback 和 useMemo 的区别?

useCallback 是「useMemo 的返回值为函数」时的特殊情况, 是 React 提供的便捷方式。在 React Server Hooks 代码 中, useCallback 就是基于 useMemo 实现的, 尽管 React Client Hooks 没有使用同一份代码, 但 useCallback 的代码逻辑和 useMemo 的代码逻辑仍是一样的