阅读 3116

为什么要在函数组件中使用React.memo? - 七日打卡

本文适合有React基础的同学食用,并且最好是实践过Hooks Api的~

关于什么是Hook,我推荐大家认真看一下官网就ok了。

这里提一下,如果大家看到这个标题有所疑惑的话,可以花点时间看一下本篇文章。反之呢如果是看到标题第一时间就反映出结论的话,就可以去get其他文章的知识点了

那么接下来就不废话了,直接长刀直入,进入正题!

初探memo

首先让我们用一个例子走进React.memo的世界

呆呆的函数组件 - 没有使用memo

对于一个函数组件来说,如果没有使用React.memo就好比是一个人没有脑子,就笨笨的呆呆的

不信我们就来看下面的Demo

点击访问演示Demo 让我们来分析下上图发生的流程:

  1. 页面第一次加载,渲染App组件和B组件,控制台打印效果如上图
  2. 点击按钮,改变App组件内的值。App组件和B组件全都发生更新

那么问题就来了,按正常逻辑来说,应该是这样的流程才对:

  1. 页面第一次渲染,App组件和B组件分别更新,并打印
  2. App组件内的数据发生变化所以,App组件重新渲染
  3. 改变的数据和B组件毛关系没有,B组件维持原状,不进行更新渲染

but理想异常丰满的,现实十分骨干的。 事实就是不但App组件发生了更新,B组件也跟着进行了更新,这不是我们想要的,因为对于B组件来说:明明老子啥都没干,却还非要我再重新穿一遍衣服?

无效渲染的原因

那么造成无效渲染的原因是啥呢?

其实简单说来是这样的:

函数组件本身没有识别prop值的能力,每次父组件更新的时候都相当于是给子组件一个新的prop。所以就相当于B组件这小子因为没带脑子(React.memo),是个呆呆的二傻子,所以他做为一个普通组件,就没有分别prop的能力,当他看到别人都更新了也就跟着把自己也造了一遍,因此就会造成上面🌰中的问题。

给憨憨带上脑子 - 使用memo进行包裹

给函数组件带上脑子 当我们给一个函数组件带上脑子的时候,就想下面这样

import React form 'react';

const FuncComponent = ()=>{

	return <h1>火热很火辣</h1>
}

export default React.memo(FunComponent);
复制代码

就不会发生上面🌰那种,无脑render组件的情况了

试着把上面demo中B组件代码里最后一行的注释放开试一下吧

然后像上面一样再次点击一下按钮,看看控制台的打印结果:

Yep! 只更新了App组件,符合预期!

那么到底是为什么造成的这种原因呢

所以到这里还不算完,让我们进一步升温🔥

激情升温 - 深入探索

到这里其实我们还是不太清楚memo是怎么做到避免无效更新的,接下来我们就来扒一扒!

class组件中的性能优化点

不知道大家有没有发现class组件中也有一个这样作用的东西,叫做PureComponent,它的功能和memo是一毛一样的。

来回顾一下,我们在class组件中经常用到的写法:

import React, {PureComponent} from 'react';

class Demo extends PureComponent {

	// 性能优化点
	shouldComponentUpdate(nextProps, nextState){	
		// 默认始终返回true
		return true;
  	}
	
	render() {
		return <h1>听懂掌声👏</h1>
  	}
	
}
复制代码

总的来说其实PureComponnetmemo都是通过对props值的浅比较来决定该组件是否需要更新的。

如果我们在class组件中,不主动使用PureComponent,也可以手动的去决定该组件是否更新,具体做法:

在生命周期shouldComponentUpdate,来通过对当前porps以及state值的对比,然后返回一个布尔值(true或者false)来决定该组件是否更新。

其实PureComponent组件就是把这对比值的部分功能帮我们完成了,方便我们直接使用,而不用再去手动的去写代码进行类似的优化。

memo的功能实现

这里是我的猜想哈,memo的原理和PureComponent应该是一样的,从开发者的角度去想,既然class组件有这样一个优化方法,那既然要推行Hook,函数组件也必定需要一个类似功能的方法去帮助大家减少代码优化的工作量。所以感觉两者在功能的实现上应该大部分都是一致的。 这里也放上一段React中PureComponent进行浅比较的代码,方便大家进一步理解

function shallowEqual (objA: mixed, objB: mixed): boolean {
  //  这里的is是判断两个值是否相等,只不过是对 + 0 和 - 0,以及 NaN 和 NaN 的情况进行了特殊的处理封装,目前react源码中好像有一套新的is判断
  if (is (objA, objB)) {
    return true;
  }
  // 判断是否为对象类型
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // 比较两个对象的,属性数量是否相等
  const keysA = Object.keys (objA);
  const keysB = Object.keys (objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // 比较两个对象的的属性是否相等,值是否相等
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call (objB, keysA [i]) ||
      !is (objA [keysA [i]], objB [keysA [i]])
    ) {
      return false;
    }
  }

  return true;
}
复制代码

这就是react中进行浅层比较的源码,也是PureComponentmemo决定是否更新组件的重要依据。

memo配合useMemo、useCallback

一般在项目的优化实践中,memo包裹的函数组件都是要配合useMemouseCallback来使用的

对于useMemo和useCallback其实我不准备长篇大幅的讲述了,因为社区已经有很多不错的文章了,大家可以搜来看一下。 我这里只做一个说人话的简单介绍就好了

useCallback

  const memoizedCallback = useCallback(
    () => {
      doSomething(a, b);
    },
    [a, b],
  );
复制代码

返回值是一个函数(memoizedCallback),这个函数就是作为第一个参数传进去的那个。区别就是作为返回值的这个函数是一个memoized的版本,用人话理解就是:保持了函数的引用。不会在组件更新时,去重新声明函数,从而改变在内存中的引用地址。

除非是第二个参数数组里的依赖项发生改变,否则这个做为返回值的函数(memoizedCallback)就一直保持原先的状态

应用场景

经常使用在父组件A向子组件B传递一个函数作为prop值的时候

父组件A:

import React,{ useCallback } form 'react';

const A = () => {
	
	return (
    	// 如果不使用useCallback包裹的话,每次A的更新,都会重新声明这个handleClick的这个函数,导致B组件无效的更新
     	<B handleClick={ useCallback( () => //doSomething,[x,xx]) };
    );
}

export default A;
复制代码

子组件B:

import React,{ memo } form 'react';

const B = (props) => {
	const { handleClick } = props;
	return <div onClick={ handleClick }>卑微小B在线被Diss</div>;
}
// 这里需要注意,要配合memo使用,否则的话不带脑子的B组件会始终认为传递过来的prop值都是一个全新的
export default memo(B);
复制代码

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码

其实和useCallback很像,只不过是useMemo返回的是一个,而不是一个函数

useCallback 的第一个参数是函数,这个函数的返回值会作为useMemo的返回值memoizedValue

除非是第二个参数数组里的依赖项发生改变,否则这个做为返回值(memoizedValue)就一直保持原先的值

useCallback能做的事useMemo都能做,但是还是推荐各司其职

const fn = useCallback( () => //doSomething , [x,xx])
// 相当于
const fn = useMemo( ()=> () => //doSomething , [x,xx])
// 因为useMemo的返回值是第一个函数的返回值,所以只要让第一个参数的函数返回一个函数就可以达到useCallback的效果
复制代码

总结

今天的内容就到这里了,多的就不在赘述了,如果还有不明白的大家可以在评论区提出讨论,或者是去看React官网,这里也是提个小建议如果你是一个React使用者,我强烈建议你认真的过一遍官网,我相信很多React的开发者连最基本的官网文档都没完整的浏览过一遍,相信我,认真去看一下会有很多收获,这也是你作为React开发者最基本的行为~

之后我所有的文章都会首发于公众号除了技术还有生活,大家可以搜索关注支持一下~

文章分类
前端
文章标签