React.memo()、useCallback()、useMemo() 区别及基本使用

更新日期: 2021-11-12阅读量: 79标签: React

先来看个简单的例子

// Parent.jsx
import react, { useState } from 'react';
import Child from '../Child';

function Parent() {
  const [parentCount, setParentCount] = useState(0);
  console.log('父組件重新渲染--------------');
  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); }}>父组件 +1</button>
    </div>
  );
}

export default Parent;
// Child.jsx
import React from 'react';

function Child() {
  console.log('------------子組件重新渲染');
  return (
    <div style={{ background: 'pink', margin: '50px 0' }}>
      <button type="button">子組件</button>
    </div>
  );
}

export default Child;

当我们点击父组件按钮时,父组件的状态parentCount会被更新,导致父组件重新渲染,子组件也会重新渲染;而此时我们的子组件和父组件之间并没有依赖关系,因此这种重复渲染是可以优化掉的,可以使用React.memo 包裹子组件

// Child.jsx
import React from 'react';
// ...other code
export default React.memo(Child);

React.memo(Comp[, fn])

用于减少子组件的重新渲染

React.memo是一个高阶组件(参数为组件,返回值为新组件的函数即为高阶组件)

对外部而言,React.memo会检查props的变更,仅当传入的props发生变化时组件才会重新渲染,这时我们再点击父组件按钮,子组件就不会重新渲染了

React.memo对复杂对象只会做浅层对比,可以通过传入第二个参数来控制对比过程

第二个参数为一个接收重新渲染前后props的函数

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

useMemo(fn[, DependentArray])

用于减少每次组件重新渲染时重复进行复杂的计算,参数为一个函数和可选的依赖项数组,返回传入函数的调用结果

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo作用类似于vue 的 computed (计算属性),不同之处在于需要手动传入依赖项,当依赖项变更时会重新调用传入的函数,返回计算值

传入依赖项为空数组时则直接返回上次的计算结果

不传入依赖项时,每次组件刷新都会重新计算,应该在代码能正常运行的情况下将其作为一种优化策略

修改下我们的例子,注意这里用React.memo 包裹了子组件,保证测试时子组件重新渲染只受传入的props变化的影响

// Parent.jsx
import React, { useState, useMemo } from 'react';
import Child from '../Child';

function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log('父組件重新渲染--------------');

  // 一个复杂的计算
  const computedFn = (a, b) => {
    console.log('----重新执行了计算----');
    return a + b;
  };

  const computedValue = useMemo(() => {
    return computedFn(parentCount, 1);
  }, [parentCount]);
  
  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child parentCount={parentCount} computedValue={computedValue} />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); }}>父组件 +1</button>
      <button type="button" onClick={() => { setOtherCount(otherCount + 1); }}>父组件 otherCount+1</button>
    </div>
  );
}

点击第一个按钮,依赖项变更,输出重新执行了计算,点击第二个按钮,因为更改的不是计算值的依赖项,因此不会重新计算,子组件也不会重新渲染

useCallback(fn[, DependentArray])

用于需要传递给子组件的函数,减少子组件的重复渲染,参数为一个函数和可选的依赖项数组,返回出入函数的记忆版本

// Parent.jsx
import React, { useState } from 'react';
import Child from '../Child';

function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log('父組件重新渲染--------------');

  const computedFn = () => {
    return parentCount + 1;
  };
  
  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child parentCount={parentCount} computedFn={computedFn} />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); }}>父组件 +1</button>
      <button type="button" onClick={() => { setOtherCount(otherCount + 1); }}>父组件 otherCount+1</button>
    </div>
  );
}

export default Parent;

// Child.jsx
import React from 'react';

function Child(props) {
  const { computedValue, computedFn } = props;
  console.log('------------子組件重新渲染');
  return (
    <div style={{ background: 'pink', margin: '50px 0' }}>
      <div>
        父组件传入的计算结果:
        {computedValue}
      </div>
      <button type="button" onClick={computedFn}>子組件</button>
    </div>
  );
}

export default React.memo(Child);

当点击第二个按钮时,子组件也会重新渲染

给computedFn 加上useCallBack

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

// ...other code

  const computedFn = useCallback(() => {
    console.log(parentCount);
    return parentCount + 1;
  }, [parentCount]) ;

// ...other code

export default Parent;

这时再点击父组件第二个按钮子组件,子组件不会重新渲染,因为useCallback 的依赖项没变更,返回的是上一次渲染的函数,因此传入子组件的props没变,组件不会重新渲染

需要注意的是,被useCallback保存的函数内部作用域也不会变更,因此,当依赖项数组为空的时候,传入useCallback的函数的内部通过闭包取的组件内的变量值终不变

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

let a = 0;
function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log('父組件重新渲染--------------');

  const computedFn = useCallback(() => {
    // 依赖项为空,这里的打印值始终不变;
    // 因为组件state变化时会重新渲染整个组件,而这里parentCount取的始终是第一次渲染版本的值
    console.log(parentCount); 
    // 这里的打印值会实时更新,因为变量直接定义在组件外部,不受组件重新渲染影响
    console.log(a);
    return parentCount + 1;
  }, []) ;

  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child parentCount={parentCount} computedFn={computedFn} />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); a += 1; }}>父组件 +1</button>
      <button type="button" onClick={() => { setOtherCount(otherCount + 1); }}>父组件 otherCount+1</button>
    </div>
  );
}

export default Parent;

因为useCallback目的是减少子组件重渲染,因此需要搭配子组件的shouldComponentUpdate或 React.memo 一起使用才有优化意义

以上是依赖项变更不频繁的情况,当依赖项变更频繁时,useCallback的记忆效果就不好,可以使用ref 作为依赖项解决

function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();

  useEffect(() => {
    textRef.current = text; // 把它写入 ref
  });

  const handleSubmit = useCallback(() => {
    // ref 对象在组件的整个生命周期内保持不变
    // 从 ref 读取它,current的变更不会引起组件的重新渲染,而函数内部又能拿到正确的值
    const currentText = textRef.current; 
    alert(currentText);
  }, [textRef]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

useRef

看看官方介绍

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变

可以理解为:用useRef创建的对象有个current 属性,这个属性就像个盒子,啥都能存,包括DOM节点;返回的 ref 对象在组件的整个生命周期内保持不变,即存在current的值不受组件重新渲染影响,始终保持着一开始的引用;同时该属性的变更也不会触发组件的重新渲染;这个属性的初始值为useRef的参数

看看官方例子

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

当把useRef 创建的对象传给DOM 元素的ref属性时,react会把当前DOM元素的引用存入current属性,这样就可以通过ref对象直接操作DOM元素了。

站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

链接: https://www.fly63.com/article/detial/10788

React常用hook的优化useEffect浅比较

先说说react原版的useEffect使用起来不便的地方,这里的effect每次更新都会执行,因为第三个参数一直是不等的,第二是在deps依赖很多的时候是真的麻烦

一文看懂React Hooks

React Hooks是从React 16.8版本推出的新特性,目的是解决React的状态共享以及组件生命周期管理混乱的问题。React Hooks的出现标志着,React不会再存在无状态组件的情况,React将只有类组件和函数组件的概念

React新Context API在前端状态管理的实践

众所周知,React的单向数据流模式导致状态只能一级一级的由父组件传递到子组件,在大中型应用中较为繁琐不好管理,通常我们需要使用Redux来帮助我们进行管理,然而随着React 16.3的发布,新context api成为了新的选择。

使用React严格模式避免过时的代码和副作用

严格模式是用于突出显示应用程序中潜在问题的工具,它不会呈现任何可见的UI。它只用于激活对其后代的额外检查和警告。严格模式不会影响生产环境。

react-router 路由切换动画

因为项目的需求,需要在路由切换的时候,加入一些比较 zb 的视觉效果,所以研究了一下。把这些学习的过程记录下来,以便以后回顾。同时也希望这些内容能够帮助一些跟我一样的菜鸟,让他们少走些坑。可能我对代码的表述不是很到位,希望大家不要介意。机智的你们一定可以看明白。

从 React 切换到 Vue.js

React 和 Vue 的关系有点像可口可乐和百事可乐,你在 React 中做的很多事情都可以在 Vue 中完成。当然这里也存在一些重要的概念差异,其中一些反映了 Angular 对 Vue 的影响。

深入解析React中的元素、组件、实例和节点

eact 中的元素、组件、实例和节点,是React中关系密切的4个概念,也是很容易让React 初学者迷惑的4个概念。现在,我就来详细地介绍这4个概念,以及它们之间的联系和区别,满足喜欢咬文嚼字、刨根问底的同学的好奇心。

React Server Component 可能并没有那么香

前段时间 React 团队发布了一项用于解决 React 页面在多接口请求下的性能问题的解决方案 React Server Components。当然该方案目前还在草案阶段,官方也只是发了视频和一个示例 demo 来说明这个草案。

react-navigation 监听顶部导航栏点击/滑动状态

使用createMaterialTopTabNavigator创建顶部导航栏,希望实现切换到指定的Tab再获取数据,查看官方文档只能找到tabBarOnPress 方法监听点击回调,但是无法监听滑动切换

21 项优化 React App 性能的技术

在 React 内部,React 会使用几项巧妙的小技术,来优化计算更新 UI 时,所需要的最少的更新 DOM 的操作。在大多数情况下,即使你没有针对性能进行专项优化,React 依然很快,但是仍有一些方法可以加速 React 应用程序

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!