零零后学React hooks

141 阅读10分钟

1. 前言

作为一名刚工作的校招萌新,一个 00 后,一个常年用 vue2 的,一个 react 小白,我对 react hooks 的学习并不深入,所以下面我将从一个初学者的角度,介绍使用 react hooks 必备的知识点和常见的一些“坑”。

2. react 独立渲染

结论 理解 react hooks 的关键是要明白 hooks 组件的每一次渲染都是独立的,也就是说每一次 render 都会有一个独立的函数作用域。

案例 1

export default function Login() {
  const [count, setCount] = useState(0);
  function changeCount(count) {
    setTimeout(() => {
      alert("count=" + count);
    }, 1000);
  }
  return (
    <div>
      <h1>count = {count}</h1>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <button onClick={() => changeCount(count)}>alert </button>
    </div>
  );
}

点击第一个按钮,变量 count 就会加 1。 点击第二个按钮,变量 count 的值就会在一秒后 alert 出来。

如图所示,如果在这一秒内再次点击两下按钮 1,就会发现 count 的值从 4 变成 6,但是弹出的结果中 count 的值并没有发生变化,这就证明了前面的结论,count 的值只与本次渲染有关,我们获取的并不是最新的值。

案例 2 知道了 react hooks 的独立渲染后,我们就会想到每次渲染后函数的声明是重复的,所以如果函数组件内的 state 和 props 无相关性,可以直接把函数声明在组件的外部。【下面代码只是提供思路,并不是推荐写法】

function changeCount(count) {
  setTimeout(() => {
    alert("count=" + count);
  }, 1000);
}
export default function Login() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>count = {count}</h1>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <button onClick={() => changeCount(count)}>alert </button>
    </div>
  );
}

注意事项

<button onClick={() => console.log(count)}>打印count</button>

根据某位同学的实习经历,他的 mentor 说像上面这种代码在组里项目是明令禁止的,无论组里项目是 react hooks,还是类式组件。

其实这是案例 1案例 2出现的共同问题,也就是这个功能看起来可以正常使用的箭头函数。如果每次渲染时都需要重新创建新的函数,即前面说的函数重复声明问题,那么根据垃圾回收机制,就需要对前面一个函数进行垃圾回收,这样就会对垃圾回收器造成一定负担。

在这一行代码中,button 的属性存在一个箭头函数,例如当类式组件使用 pureComponent 纯组件进行性能优化时,那么这个 button 属性中的箭头函数就会让 react 认为每一次渲染都需要创建一个新的箭头函数,也就是说这样的写法会导致 pureComponent 完全失效,引起不必要的重复渲染。

至于解决办法,也很简单,把箭头函数提出去,类式组件提到 render 方法外面,函数组件就是提到函数外面。

案例 3

const changeCount = (count) => {
  return () => {
    setTimeout(() => {
      alert("count=" + count);
    }, 1000);
  };
};
export default function Login() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>count = {count}</h1>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <button onClick={changeCount(count)}>alert </button>
    </div>
  );
}

可以看到,案例 3 的代码并没有完全解决所谓的箭头函数问题,setCount 函数为什么没有放在函数组件外呢?

hhhh,因为 setCount 并非普通函数,而是 useState 提供的,无法脱离函数组件本身。所以案例 3 的代码还不是最终的解决方案。

3. useState 的异步更新问题

作为使用 react 的开发者,每次修改 state 的数据,都不会立即在页面上显示更新,更新后立即打印同样也显示的是修改前的值。​ 这是因为 react 的事件分为合成事件和原生事件,原生事件是同步的,合成事件和用的钩子函数的都是异步的,所以我们在调用 setState 的过程中不要试图去立即获取数据状态的变化。 ​ 在真实场景下我们使用 setState 修改数据时会依赖于旧的数据,这时通常采用 setState 的函数形式。这样,我们就相当于告诉 react,count 只是有一个递增状态,count 值的变化不应该影响 useEffect 的值,也就可以让 count 从依赖项数组里去除,不会造成定时器的重复开启和清除。

<button
  onClick={() =>
    setCount((count) => {
      return count + 1;
    })
  }
>
  count + 1
</button>

4. useEffect、useMemo、useCallback 三兄弟

useEffect

说到 react hooks,真正在项目中最常用的其实只有 useEffect 和 useState。上文提到 hooks 的每一次渲染都是独立的,那么一个 hooks 组件是如何把每一次渲染关联起来的呢?答案就是 useEffect。 ​ useEffect 是一个可以让开发者在函数组件里执行副作用的 hook,副作用是在每次组件渲染之后才会生效,第一个参数是要执行的函数体,第二个参数是一个依赖项数组。

结论:

  1. 第二个参数为空数组,首先渲染后会执行函数体。
  2. 第二个参数不写,每次渲染后会都会执行。
  3. 填了依赖项之后,只有依赖项发生改变才会执行。
  4. useEffect 的函数体可以返回一个函数,叫清除函数,作用是消除副作用,也就是在下一个 effect 执行之前,消除上一个 effect。

定时器(每隔 1 秒 count 值加 1)案例

不推荐写法

useEffect(() => {
  const intervalId = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(intervalId);
}, [count]);

不推荐原因 每次重新执行都需要把定时器清除掉,然后开一个新的定时器。

解决方案

useEffect(() => {
  const intervalId = setInterval(() => {
    setCount((count) => count + 1);
  }, 1000);
  return () => clearInterval(intervalId);
}, []);

采用 setState 的函数形式,告知 react count 值有一个递增状态,但并不关心 count 值的变化。

所以,count 可以从 useEffect 的依赖项中去掉,这时 effect 只会执行一次。

useMemo

与 useEffect 写法很类似,第一个参数是 callback 函数,第二个参数是一个依赖项数组,根据数组里的依赖项是否发生变化来判断是否需要更新回调函数,这听起来是不是很像 useEffect。

区别在于 useMemo 是记录函数体的返回值,避免大量的重复计算,这与 useEffect 完全不同。

useMemo 的使用是有代价的,不可以滥用,复杂度特别低的重复计算可以不用 useMemo。

案例

function TodoList({ param1,param2 }) {
    const computedParam = useMemo(() => {
          return func( param1, param2);
    }, [param1,param2]);
   .......
}

当 func 是一个复杂度较高的计算函数时,采用 useMemo 可以减少不必要的循环和重复计算,避免很多不必要的开销。

useCallback

useCallback 与 useMemo 极其相似,唯一的不同点是 useMemo 返回的是函数运行的结果,useCallback 返回是一个函数。经过 useCallback 包裹的函数,在依赖项不发生改变时,函数不会再次刷新,这就为我们前面案例 1 中描述的函数重复声明的问题提供了一个新的解决思路,用 useCallback 包裹函数。代码如下。

案例 4(推荐写法):

export default function Login() {
  const [count, setCount] = useState(0);
  const sCount = useCallback((count) => {
    setCount(count + 1);
  }, []);
  const changeCount = useCallback((count) => {
    setTimeout(() => {
      alert("count=" + count);
    }, 1000);
  }, []);
  return (
    <div>
      <h1>count = {count}</h1>
      <button onClick={sCount(count)}>count + 1</button>
      <button onClick={changeCount(count)}>alert </button>
    </div>
  );
}

通过使用 useCallback,依赖项为空数组,所以 sCount 函数只会在首次渲染时声明一次。

5. useRef 的妙用

使用 useRef 可以获取当前元素的所有属性,并且返回一个可变的 ref 对象,并且这个对象只有 current 属性,可以设置初始值。 除了获取对应的属性值外,useRef 还有一个比较重要但很容易忽视的特性,那就是缓存数据。与之前学过的 createRef 不同的地方在于 useRef 每次返回的是相同的引用,ref 对象的值发生改变,不会触发组件的重新渲染。 在封装一个自定义 hooks 时,根据案例 1里提到的定时器案例,用 useState 实现变量的控制,获取变量并不是最新的,如果需要获取更新后的值则需要让整个组件重新渲染,在这种情况下,用 useRef 将会是一个不错的选择。为了随时确保获取的值是最新的,我们可以进行一个简单的封装。

const useLastest = (value) => {
  const ref = useRef(value);
  ref.current = value;
  return ref;
};

类似的,我们可以结合 useEffect 的特性,组件渲染后才会执行本次副作用。

const usePrevious = (value) => {
  const ref = useRef(value);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
};

所以 return ref current 指向的是 value 改变之前的值。 使用 usePrevious,可以实现记录函数组件前一次渲染的 state。

6. useEffect 的滥用

首先,copy 一下 react 新官网的一句话。 ​ If there is no external system involved (for example, if you want to update a component’s state when some props or state change), you shouldn’t need an Effect。 简单翻译一下,就是有这样一个场景。当 props 和 state 发生改变时,你需要更新组件的状态,这时不应该使用 useEffect。

提出一个场景

当某个项目的一个函数组件接受到 props 时,并且需要根据 props 的值来计算一个变量时。一个 react 萌新直接在顶层用了一个函数,参数是 props,计算出了变量值。

一个 react"大佬"看后笑了笑,直言 props 很少改变,你这不是重复计算了?看我的!【了却前端天下事,赢得生前身后名,可怜小萌新!】

萌新的写法:

function TodoList({ param1,param2 }) {
   const computedParam = func( param1, param2);
  .......
}

“大佬”的写法:

function TodoList({ param1,param2 }) {
  const [computedParam, setComputedParam] = useState([]);
    useEffect(() => {
          setComputedParam( func( param1, param2));
    }, [param1,param2]);
   .......
}

"大佬"的错误:

react 官方认为 useEffect 处理的是组件渲染后的事情,如果在 useEffect 里再次执行了改变 state 的方法,那么 react 需要再进行一次渲染,也就是说 react 上一次的渲染,上一次对 DOM 的修改完全是不必要的,这就是重复渲染!

我的写法:

useMemo 的返回值是函数体的返回结果,当 param1,param2 的值发生改变时,才会重新计算变量的值,这不就达到我们的要求了吗?

function TodoList({ param1,param2 }) {
    const computedParam = useMemo(() => {
          return func( param1, param2);
    }, [param1,param2]);
   .......
}

具体案例推荐看 react 新官方文档: beta.reactjs.org/learn/you-m…

当你认真看了新官方文档后,你就会发现对于萌新的写法,react 的评价是 在很多情况下,这段代码很好!

是的!即使存在重复计算的问题,但如果这只是一个复杂度较低的计算,那么你可能不需要 useMemo,因为 useMemo 的使用也是有代价的,强行使用只会造成组件性能的下降。

7. 总结

  1. 使用 useState 不追求立即获取修改后的值。
  2. 标签上的事件绑定【极不推荐】采用箭头函数的形式,因为这会造成整个组件必定会重复渲染。
  3. useEffect 中不应该出现改变 state 的函数,因为这代表组件上一次渲染是完全不必要的,造成重复渲染问题。
  4. useMemo 记录的是函数体的返回值,useCallback 记录的是函数本身,可以解决 使用 react hooks 的函数重复创建,并且不能把函数提取到函数组件外 的问题。
  5. 不需要在 Effects 里来处理用户事件,因为在 Effect 运行时,您不知道用户做了什么(例如,单击了哪个按钮)。
  6. useRef 是一个神奇的工具,使用 useRef 可以获取当前元素的所有属性,可以获取到最新的元素属性,并且 ref 对象的值发生改变,不会触发组件的重新渲染。
  7. useMemo 和 useCallback 的使用是有代价的,过度的使用可能会造成组件的性能还不如有重复计算或者重复创建函数的组件。
作者简介

李长沁:零零后小萌新