专栏首页IMWeb前端团队可能你的react函数组件从来没有优化过

可能你的react函数组件从来没有优化过

本文作者:IMWeb lhyt 原文出处:IMWeb社区 未经同意,禁止转载

16.6之前,函数组件没有像shouldComponentUpdate这样的方法,也没有类似PureComponent这种解决方案,避免不了函数组件里面所有的代码再次的执行,要依靠外面的条件渲染来控制,或者是高阶组件。之前的话,选择使用函数组件的情况是一些比较简单的又比较纯的组件,只是负责展示的。而且函数组件最终编译babel结果是只执行createElement那一步;class组件一样有生命周期要实例化,最终经过Babel成es5代码的时候还很长

React.memo

当16.6的memo问世,函数组件就有了类似PureComponentshouldComponentUpdate的解决方案,memo的使用方法:

const C = (props) => {
    return <section>那一夜{props.name}真帅</section>
}

export default React.memo(C)

当父组件执行render的时候,避免不了C组件的渲染和C函数的执行(如果不在外面加判断的话:{isShowC && <C />})。当到了C组件的时候,会浅比较C组件前后props值。如果props每一个属性值都一样,会跳过函数组件C的执行,减少了不必要的渲染,达到了性能优化。

memo第二个参数

第二个参数,是一个函数,该函数传入参数是新props和上次props,我们可以在函数里面做判断逻辑,控制返回值。当我们让函数return true的时候,告诉了react这两个props是一样的,不用重新执行整个函数组件;反之false的时候会重新执行该组件

memo(IfEqual, () => false);

比如这行代码,判断函数一直返回false,memo包住的IfEqual组件无论怎样都会重新执行

当我们用上了memo,就可以根据业务来进行优化了:

React.memo(C, (nextProps, prevProps) => {
    // 做我们想做的事情,类似shouldComponentUpdate
})

函数组件中传入的props值为函数时

我们都知道,js中函数不是简单数据类型,也就是说function(){}function(){}是不一样的,与{}{}不一样同理。那么我们传入props.onClick(即使是长得一样的内容完全一样),前后props.onClick都不能划上等号

    <div>
      <IfEqual onClick={() => {}} />
    </div>

觉得inline function不好看,那前面定义一下,实际上还是逃不了同一个事情:它们是不一样的。这次是因为,函数组件的渲染,也就是执行,每一次重新执行,函数作用域里面一切都是重新开始。这就相当于上一次组件渲染const handleClick = () => {},后面渲染又一次const handleClick = () => {},它们都不是同一个东西

export default () => {
  const handleClick = () => {} 
  return (
    <div>
      <IfEqual onClick={handleClick} />
    </div>
  )
}

这种情况下,我们可以用memo第二个参数来拯救多余一次的渲染的局面:

// props: { a: 1, onClick: () => {} }
// 在我们知道onClick是做同一个事情的函数的前提下,不比较onClick
React.memo(C, (nextProps, prevProps) => nextProps.a === prevProps.a)

最后,前后props的onClick,它们只有一种情况是一样的——把声明抽到组件外面去

const handleClick = () => {} 
export default () => {
  return (
    <div>
      <IfEqual onClick={handleClick} />
    </div>
  )
}

这时候,有没有想起class组件里面总是呢?this.handleClick一直都是同一个函数。这种情况,子组件为函数组件的时候,包一层memo就可以实现purecomponent的效果

useCallback

函数组件把函数定义写在外面,是可以解决问题。但是,如果handleClick依赖组件内部的一些变量,那handleClick又不得不写在里面(当然利用引用类型可以解决)。或者还是正常写,靠memo第二个参数来控制要不要重新渲染子函数组件。但是无论怎样,都存在一个问题,就是那一块代码写在里面呢,都无法避免代码的执行和函数的重新定义,比如

function a(){
    const b = function(){
        console.log(1)
        // 很多很多代码
    }
}
a()
a() // 函数b又被定义了一次

如果我们通过依赖来确定前后两次是不是同一个函数,我们可以用函数记忆来实现整个功能

// 半伪代码
let prev
let prevDeps
function memorize(fn, deps) {
    // 前后依赖一致,不用重新计算直接返回上次结果
    if (prev && isEqual(deps, prevDeps)) {
        return prev
    }
    prevDeps = deps
    prev = fn
    return fn
}

function a(){
    const b = memorize(function(){
        console.log(1)
        // 很多很多代码
    }, [])
}
a()
a() // 函数b是同一个

类似函数记忆的原理,后来有了useCallback的出现,多了一种新的解决方案,根据依赖生成一个函数:

const handleClick = useCallback(() => {
    console.log(dep)
}, [dep])

当dep不变,每一次函数组件的执行,handleClick都是同一个函数。如果dep变了,那么handleClick又是一个新的函数

export default () => {
// 没有依赖,永远是同一个函数
const handleClick = useCallback(() => {}, []);

// 依赖a,重新执行函数组件,a不变的,是同一个函数
// a变了handleClick是新的函数
const handleClick1 = useCallback(() => {}, [a]);
  return (
    <div>
      <IfEqual onClick={handleClick} />
    </div>
  )
}

react组件也是一个函数,那其实useCallback还可以做一个函数组件:

export default () => {
const handleClick = useCallback(() => {}, []);
const Cpn = useCallback(({ name }) => {
    return <button onClick={handleClick}>{name}</button>
}, [handleClick]);

  return (
    <div>
      <Cpn name="hi" />
    </div>
  )
}

当然这只是一个简单的场景,如果用了hooks,还没有解决问题或者暂时没有想到优雅的封装技巧,想用高阶组件的时候,不妨尝试一下useCallback

useMemo

const a = useMemo(() => memorizeValue, deps)

当deps不变,a的值还是上次的memorizeValue,省去了重新计算的过程。如果memorizeValue是一个函数,和useCallback是一样的效果:

useCallback(fn, inputs) <=> useMemo(() => fn, inputs)

我们可以试一下同步执行的代码,当时间非常长的时候,useMemo可以发挥它的作用了:

// 强行更新组件
const useForceUpdate = () => {
  const forceUpdate = useState(0)[1];
  return () => forceUpdate(x => x + 1);
}
// 一个很耗时间的代码
function slowlyAdd(n) {
  console.time('add slowly')
  let res = n;
  for (let i = 0; i < 2000000000; i++) {
    res += 1;
  }
  console.timeEnd('add slowly')
  return res;
}

// useMemo记忆结果的一个自定义hook
function useSlowlyAdd(n) {
  const res = useMemo(() => {
    return slowlyAdd(n);
  }, [n])
  return res;
}

export default () => {
  const [count, add] = useState(1);
  const forceUpdate = useForceUpdate();
  const handleClick = useCallback(() => {}, []);
  useSlowlyAdd(count) // 第一次这里会耗很多时间,页面卡死一阵
  return (
    <>
      <button onClick={forceUpdate}>更新页面</button>
      <button onClick={() => add(count + 1)}>+</button>
    </>
  )
}

第一次进来,页面暂时没有任何反应一阵,这是因为slowlyAdd占用了js主线程。当我们点击‘更新页面’更新的时候,页面并没有卡死,而且组件也重新渲染执行了一次。当我们点击+,页面又开始卡死一阵。

这是因为点击+的时候,修改了useMemo的依赖n,n变了重新计算,计算耗费时间。如果点击更新页面,没有修改到依赖n,不会重新计算,页面也不会卡

当然,useMemo也可以做高阶组件,用起来的时候,可以写成reactElement的形式了:

const HOC = useMemo(() => <C />, deps)

最后

有如下的组件,Big是一个10w个节点的组件,每一个节点都绑定事件

const handleClick = useCallback(() => {}, []);
export default () => {
  return (
    <div>
      <Big onClick={handleClick} />
    </div>
  )
}

如果Big组件没有memo包住,首次挂载和再次渲染父组件性能如下:

如果Big组件有memo包住而且props被认为是一样的情况下,首次挂载和再次渲染父组件性能如下:

但是性能优化不是免费午餐,不是所有的函数组件都包memo,组件里面的函数都包usecallback就好了。因为具有memorize,没有优化的意义的情况下强行优化,反而是性能恶化。

总结一下对于props的某个属性值为函数的时候,如何做到子组件不重新执行多余渲染:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Lego组件平台开发(一)

    本文作者:IMWeb 刘恒兵 原文出处:IMWeb社区 未经同意,禁止转载 Lego组件平台开发(一) @(lego平台) 为什么要做组件平台 为什么...

    IMWeb前端团队
  • 人人都是艺术家!谈谈那些奇怪的字符(上)

    前言 编码,是每个程序员绕不开的话题。对于前端工程师而言,字符更是会直观地展示在界面上。 提起文字,大部分人的脑中,都会定式为规整排列的字符。但是林子大了什么鸟...

    IMWeb前端团队
  • Lego组件平台开发(一)

    然后,在任何产品的上线过程中,谁都不愿意重复早轮子,都希望能通过一些规范和标准统一起来,后续就完全按照这个标准执行,并能否把历史上实现过的沉淀出来的直接使用,不...

    IMWeb前端团队
  • 提示可能你的react函数组件从来没有优化过React.memome

    当16.6的memo问世,函数组件就有了类似PureComponent和shouldComponentUpdate的解决方案,memo的使用方法:

    lhyt
  • 解析SwiftUI布局细节(二)循环轮播+复杂布局

    上一篇我们总结的主要是VStack里面的东西,由他延伸到 @ViewBuilder, 接着我们上一篇总结的我们这篇内容主要说的是下面的几点,在这些东西说完后我准...

    Mr.RisingSun
  • Springboot 之基于腾讯云 Serverless 的订单应用

    前言 这是一个 JAVA 开发的订单后台应用(没错!就是那个让无数大学生痛不欲生的订单后台系统),结合 Serverless 这一无服务器思想,尝试通过云函数...

    腾讯云serverless团队
  • 每个 PHPer 都应当掌握的注释标记

    注释标签在代码注释中的作用非常大,但是可能很多同学在平常开发中会忽略这些标签的作用,所以我这边特地整理一些常用的注释标记,通过图文展现形式,希望能帮助你能更好理...

    猿哥
  • 远程调试手机页面工具:Weinre 介绍

    Weinre(全称Web Inspector Remote)是一款的可以在电脑上远程调试手机页面的工具。

    Joel
  • 如何从 Ceph (Luminous) 集群中安全移除 OSD

    工作中需要从 Ceph 的集群中移除一台存储服务器,挪作他用。Ceph 存储空间即使在移除该存储服务器后依旧够用,所以操作是可行的,但集群已经运行了很长时间,每...

    用户2443625
  • 使用case语句编写系统服务脚本

    case语句可以很好的代替if语句中的多分支结构,使用case分支语句时,有几个值得注意的特点如下所述:

    小手冰凉

扫码关注云+社区

领取腾讯云代金券