关于前端:详细解读-React-useCallback-useMemo

28次阅读

共计 5870 个字符,预计需要花费 15 分钟才能阅读完成。

前言

浏览本文章须要对 React hooks 中 useState 和 useEffect 有根底的理解。我的这篇文章内有大抵介绍 在 React 我的项目中全量应用 Hooks。

useCallback

useCallback 的作用

官网文档:

Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.

简略来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。

useCallback 的利用

在线代码:Code Sandbox

import React, {useState, useCallback} from 'react';
import Button from './Button';

export default function App() {const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const [count3, setCount3] = useState(0);

  const handleClickButton1 = () => {setCount1(count1 + 1);
  };

  const handleClickButton2 = useCallback(() => {setCount2(count2 + 1);
  }, [count2]);

  return (
    <div>
      <div>
        <Button onClickButton={handleClickButton1}>Button1</Button>
      </div>
      <div>
        <Button onClickButton={handleClickButton2}>Button2</Button>
      </div>
      <div>
        <Button
          onClickButton={() => {            setCount3(count3 + 1);          }}        >          Button3        </Button>
      </div>
    </div>
  );
}
// Button.jsx
import React from 'react';

const Button = ({onClickButton, children}) => {
  return (
    <>
      <button onClick={onClickButton}>{children}</button>
      <span>{Math.random()}</span>
    </>
  );
};

export default React.memo(Button);

在案例中能够别离点击 Demo 中的几个按钮来查看成果:

  • 点击 Button1 的时候只会更新 Button1 和 Button3 前面的内容;
  • 点击 Button2 会将三个按钮后的内容都更新;
  • 点击 Button3 的也是只更新 Button1 和 Button3 前面的内容。

上述成果认真理一理就能够发现,只有通过 useCallback 优化后的 Button2 是点击本身时才会变更,其余的两个只有父组件更新后都会变更(这里 Button1 和 Button3 其实是一样的,无非就是函数换了个中央写)。上面咱们认真看看具体的优化逻辑。

这里或者会留神到 Button 组件的 React.memo 这个办法,此办法内会对 props 做一个浅层比拟,如果如果 props 没有产生扭转,则不会从新渲染此组件。

const a = () => {};
const b = () => {};
a === b; // false

上述代码能够看到咱们两个一样的函数却是不相等的(这是个废话,我置信能看到这的人都晓得,所以不做解释了)。

const [count1, setCount1] = useState(0);
// ...
const handleClickButton1 = () => {setCount1(count1 + 1);
};
// ...
return <Button onClickButton={handleClickButton1}>Button1</Button>

回头再看下面的 Button 组件都须要一个 onClickButton 的 props,只管组件外部有用 React.memo 来做优化,然而咱们申明的 handleClickButton1 是间接定义了一个办法,这也就导致只有是父组件从新渲染(状态或者 props 更新)就会导致这里申明出一个新的办法,新的办法和旧的办法只管长的一样,然而仍旧是两个不同的对象,React.memo 比照后发现对象 props 扭转,就从新渲染了。

const handleClickButton2 = useCallback(() => {setCount2(count2 + 1);
}, [count2]);

上述代码咱们的办法应用 useCallback 包装了一层,并且前面还传入了一个 [count2] 变量,这里 useCallback 就会依据 count2 是否发生变化,从而决定是否返回一个新的函数,函数 外部作用域 也随之更新。

因为咱们的这个办法只依赖了 count2 这个变量,而且 count2 只在 点击 Button2 后才会更新 handleClickButton2,所以就导致了咱们点击 Button1 不从新渲染 Button2 的内容。

Tips

import React, {useState, useCallback} from 'react';
import Button from './Button';

export default function App() {const [count2, setCount2] = useState(0);

  const handleClickButton2 = useCallback(() => {setCount2(count2 + 1);
  }, []);

  return (<Button       count={count2}
      onClickButton={handleClickButton2}
    >Button2</Button>
  );
}

咱们调整了一下代码,将 useCallback 依赖的第二个参数变成了一个 空的数组,这也就意味着这个办法没有依赖值,将不会被更新。且因为 JS 的动态作用域导致此函数内 count2 永远都 0

能够点击屡次 Button2 查看变动,会发现 Button2 前面的值只会扭转一次。因为上述函数内的 count2 永远都是 0,就意味着每次都是 0 + 1,Button 所承受的 count props,也只会从 0 变成 1且始终都将是 1,而且 handleClickButton2 也因没有依赖项不会返回新的办法,就导致 Button 组件只会因 count 扭转而更新一次。


上述提到的是不更新所带来的问题,接下来在看一个频繁更新所带来的问题。

const  = useState('');

const handleSubmit = useCallback(() => {// ...}, );

return (
  <form>
    <input value={text} onChange={(e) => setText(e.target.value)} />    <OtherForm onSubmit={handleSubmit} />
  </form>
);

上述例子中能够看到咱们的 handleSubmit 会依赖 text 的更新而去更新,在 input 的应用中 text 的变动必定是相当频繁的,如果这时候咱们的 OtherForm 是一个很大的组件,必须要进行优化这个时候能够应用 useRef 来帮忙。

const textRef = useRef('');
const  = useState('');

const handleSubmit = useCallback(() => {console.log(textRef.current);
  // ...
}, [textRef]);

return (
  <form>
    <input value={text} onChange={(e) => {const { value} = e.target;      setText(value)      textRef.current = value;    }} />    <OtherForm onSubmit={handleSubmit} />
  </form>
);

应用 useRef 能够生成一个变量让其在组件每个生命周期内都能拜访到,且 handleSubmit 并不会因为 text 的更新而更新,也就不会让 OtherForm 屡次渲染。


评论中有为敌人提到是否要把所有的办法都用 useCallback 包一层呢,这个应该也是很多刚理解 useCallback 的敌人的一疑难。先说答案是:不要把所有的办法都包上 useCallback,上面认真讲。

const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

const handleClickButton1 = () => {setCount1(count1 + 1)
};
const handleClickButton2 = useCallback(() => {setCount2(count2 + 1)
}, [count2]);

return (
  <>
    <button onClick={handleClickButton1}>button1</button>
    <button onClick={handleClickButton2}>button2</button>
  </>
)

参考 前端进阶面试题具体解答

下面这种写法在以后组件从新渲染时会申明一个新的 handleClickButton1 函数,上面 useCallback 外面的函数也会申明一个新的函数,被传入到 useCallback 中,只管这个函数有可能因为 inputs 没有产生扭转不会被返回到 handleClickButton2 变量上。

那么在咱们这种状况它返回新的函数和老的函数也都一样,因为上面 <button> 曾经都会被渲染一下,反而应用 useCallback 后每次执行到这里外部要要比对 inputs 是否变动,还有存一下之前的函数,耗费更大了。

useCallback 是要配合子组件的 shouldComponentUpdate 或者 React.memo 一起来应用的,否则就是反向优化。

useMemo

useMemo 的作用

官网文档:

Pass a“create”function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed.

简略来说就是传递一个创立函数和依赖项,创立函数会须要返回一个值,只有在依赖项产生扭转的时候,才会从新调用此函数,返回一个新的值。

useMemo 的利用

useMemo 与 useCallback 很像,根据上述 useCallback 曾经能够想到 useMemo 也能针对传入子组件的值进行缓存优化。

// ...
const [count, setCount] = useState(0);

const userInfo = {
  // ...
  age: count,
  name: 'Jace'
}

return <UserCard userInfo={userInfo}>
// ...
const [count, setCount] = useState(0);

const userInfo = useMemo(() => {
  return {
    // ...
    name: "Jace",
    age: count
  };
}, [count]);

return <UserCard userInfo={userInfo}>

很显著的下面的 userInfo 每次都将是一个新的对象,无论 count 产生扭转没,都会导致 UserCard 从新渲染,而上面的则会在 count 扭转后才会返回新的对象。

上述用法是有有对于父子组件传值带来的性能优化,实际上 useMemo 的作用 不止于此,依据官网文档内介绍:

This optimization helps to avoid expensive calculations on every render.

能够把一些低廉的计算逻辑放到 useMemo 中,只有当依赖值产生扭转的时候才去更新。

const num = useMemo(() => {
  let num = 0;
  // 这里应用 count 针对 num 做一些很简单的计算,当 count 没扭转的时候,组件从新渲染就会间接返回之前缓存的值。return num;
}, [count]);

return <div>{num}</div>

事实上在应用中 useMemo 的场景远比 useCallback 要宽泛的很多,咱们能够将 useMemo 的返回值定义为返回一个函数这样就能够变通的实现了 useCallback。在开发中当咱们有局部变量扭转时会影响到多个中央的更新那咱们就能够返回一个对象或者数组,通过解构赋值的形式来实现同时对多个数据的缓存。

const [age, followUser] = useMemo(() => {
  return [new Date().getFullYear() - userInfo.birth, // 依据生日计算年龄
    async () => { // 关注用户
      await request('/follow', { uid: userInfo.id});
      // ...
    }
  ];
}, [userInfo]);

return (
  <div>
    <span>name: {userInfo.name}</span>
    <span>age: {age}</span>
    <Card followUser={followUser}/>
    {useMemo(() => (// 如果 Card1 组件外部没有应用 React.memo 函数,那还能够通过这种形式在父组件缩小子组件的渲染        <Card1 followUser={followUser}/>
      ), [followUser])    }  </div>
)

结语

简略了解呢 useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回值。useCallback 是来优化子组件的,避免子组件的反复渲染。useMemo 能够优化以后组件也能够优化子组件,优化以后组件次要是通过 memoize 来将一些简单的计算逻辑进行缓存。当然如果只是进行一些简略的计算也没必要应用 useMemo,这里能够思考一些计算的性能耗费和比拟 inputs 的性能耗费来做一个衡量(如果真有逻辑跟这个比拟逻辑差不多,也没必要应用 useMemo,还能缩小一点对键盘磨损 😅)。

正文完
 0