Bootstrap

useCallback别乱用! 性能优化也是有成本的.

前言

我们在得知useCallback能够优化性能后,恨不得每个函数都要拿useCallback包裹一下;不过我们需要明白: 错误或盲目的使用useCallback会导致性能不升反降;

useCallback这种memoized函数也是需要成本的,比如增加了额外的deps变化判断,再比如可能会获得更多的内存分配…; 因此性能优化带来的好处可能抵消不了它的成本; 打个比方就像你开车去10公里以外的小镇 和 你开车去隔壁的邻居家);

那么,什么情况下才可以使用useCallback去进行性能优化呢?
在你觉得这种性能的交换比较合理的时候.常用的场景比如:

函数 functionA 在 useEffect 内部被调用,为了避免 useEffect 的频繁触发,所以我用useCallback将函数 functionA 包起来;

函数 functionB 在另一个使用useCallback包裹的函数 functionC 中被调用,为了避免这个函数functionC频繁变更,所以我用useCallback将函数 functionB 包起来;

当函数 functionD 被传递给子组件,为了避免子组件进行不必要的频繁渲染,所以我用useCallback将函数functionD 包起来[※]

正文

场景一
在某些场景里需要在useEffect里调用一个函数,出于某些原因.你无法把这个函数移动到effect内部;

const Example = ({ someProp }) => {

 const doSomething = () => {
   console.log(someProp);
 }

 useEffect(() => {
   doSomething();
 }, [doSomething]); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}

因为useEffect中调用的 doSomething 函数使用了 someProp这个props,所以需要将doSomething放在useEffect的依赖列表里;

而为了确保这个函数不会随着组件渲染而重新创建,我们需要用useCallback把doSomething包裹起来,如下:

const Example = ({ someProp }) => {
  // ✅ 用 useCallback 包裹以避免随渲染发生改变
  const doSomething = useCallback(() => {
    // ... Does something with someProp ...
  }, [someProp]); // ✅ useCallback 的所有依赖都被指定了

 useEffect(() => {
    doSomething();
  }, [doSomething]); // ✅ useEffect 的所有依赖都被指定了
}

场景二
类似于场景一, 在某些情况下,你需要在useCallback包裹的函数中调用另一个函数,记得要给另一个函数也包上useCallback

const Example = ({ prop1, prop2 }) => {
 // ✅ 用 useCallback 包裹以避免随渲染发生改变
 const something = useCallback(() => {
 // ... Does something with prop2 ...
 }, [prop2]); // ✅ useCallback 的所有依赖都被指定了
 
 // ✅ 用 useCallback 包裹以避免随渲染发生改变
 const doSomething = useCallback(() => {
 	somethingOne();
   // ... Does something with prop1 ...
 }, [prop1, somethingOne]); // ✅ useCallback 的所有依赖都被指定了

useEffect(() => {
   doSomething();
 }, [doSomething]); // ✅ useEffect 的所有依赖都被指定了
}

场景三
当函数 functionD 被传递给子组件:

const ParentComp = () => {
  const [ count, setCount ] = useState(0)
  const increment = () => setCount(count + 1)
  const [ name, setName ] = useState('hi~')
  const changeName = (newName) => setName(newName)

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <ChildComp name={name} onClick={changeName}/>
    </div>
  );
}

const ChildComp = ({ name, onClick }) => {
  console.log('render child-comp ...')
  return <>
    <div>Child Comp ... {name}</div>
    <button onClick={() => onClick('hello')}>改变 name 值</button>
  </>
}

我们先来看一下组件触发re render的时机

  • 组件的state的属性改变时;
  • 组件的props的任意属性值改变时;
  • 父组件重新rerender时;

根据组件的从新渲染机制,我们再来重新看一下代码:

const ParentComp = () => {
  const [ count, setCount ] = useState(0)
  const increment = () => setCount(count + 1)
  const [ name, setName ] = useState('hi~')
  const changeName = (newName) => setName(newName)  // 父组件渲染时会创建一个新的函数

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <ChildComp name={name} onClick={changeName}/>
    </div>
  );
}

const ChildComp = ({ name, onClick }) => {
  console.log('render child-comp ...')
  return <>
    <div>Child Comp ... {name}</div>
    <button onClick={() => onClick('hello')}>改变 name 值</button>
  </>
}

会发现,当我点击父组件的button来增加次数时,纵使子组件的state和props没有任何变化,子组件依然会rerender,这显然不是我们所期待的,原因我们也清楚,因为父组件重新rerender (state变化,父组件rerender) 且 子组件的props的onClick属性发生了变化 (父组件rerender时,函数changeName重新生成);

我们可以通过React.memo结合useCallback来解决子组件不必要rerender的问题

  • 通过React.memo包裹子组件来解决因 父组件重新rerender 导致子组件rerender的问题;
  • 通过useCallback包裹函数changeName来解决因 子组件的props的onClick属性发生了变化 导致子组件rerender的问题;
const ParentComp = () => {
  const [ count, setCount ] = useState(0)
  const increment = () => setCount(count + 1)
  const [ name, setName ] = useState('hi~')
   const changeName = useCallback((newName) => setName(newName), []) // ✅ 通过useCallback将函数changeName包裹

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <ChildComp name={name} onClick={changeName}/>
    </div>
  );
}

const ChildComp = React.memo(({ name, onClick }) => {  // ✅ 通过React.memo 高阶组件将子组件包裹
  console.log('render child-comp ...')
  return <>
    <div>Child Comp ... {name}</div>
    <button onClick={() => onClick('hello')}>改变 name 值</button>
  </>
})

[注:] React.memo和useCallback缺一不可;

;