方凳雅集
首发于方凳雅集

React Hooks下render次数的优化

开篇先接单介绍一下Hooks先,Hooks是从16.8版本之后React的新特性。不需要用class来开发组件了,让代码更加简洁。但是这个API是React后面的核心,还是要去好好了解一下的。

如果同学你看此文的时候还没有了解过,没写过Hooks的话,建议先看一下官方文档。先了解基本的写法,运行机制才比较好懂下文的一些知识点。

首先我们来定义父子两个组件:DC,JOKER。我们先用class的方式去实现,这里用PureComponent,因为不需要再考虑shouldComponentUpdate。

import React, { PureComponent, } from 'react';

class JOKER extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      count: props.count,
    }
  }

  componentWillReceiveProps(nextProps) {
    console.log('I am JOKER\'s componentWillReceiveProps--->');
    this.setState({ count: nextProps.count });
  }

  render() {
    console.log('I am JOKER\'s render--->');
    const { count } = this.state;
    return (
      <div>
        <p style={{ color: 'red' }}>JOKER: You clicked {count} times</p>
      </div>
    );
  }
}

class DC extends PureComponent {
  constructor() {
    super();
    this.state = {
      count: 0,
    };
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <p>You clicked {count} times</p>
        <button onClick={() => this.setState({ count: count + 1, })}>
          Click me
        </button>
        <JOKER count={count} />
      </div>
    );
  }
}

DEMO的逻辑很简单,就是DC里面有一个按钮,点击按钮更新内部状态count,JOKER收到props的更新之后,也对自己的state进行一个更新。效果如下:

收到新的props,执行componentWillReceiveProps,然后setState,最后render。componentWillReceiveProps,render都只是执行一次,从控制台上看到的日志也全都是符合预期。

那好,我们将这段逻辑改用Hooks来写,还是同样的DC,JOKER。

import React, { useState, useEffect, useMemo, } from 'react';

function JOKER(props) {
  const [count, setCount] = useState(props.count);
  useEffect(() => {
    console.log('I am JOKER\'s useEffect--->', props.count);
    setCount(props.count);
  }, [props.count]);

  console.log('I am JOKER\'s  render-->', count);
  return (
    <div>
      <p style={{ color: 'red' }}>JOKER: You clicked {count} times</p>
    </div>
  );
}

function DC() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => {
        setCount(count + 1);
        console.log('\n');
      }}>
        Click me
      </button>
      <p>DC: You clicked {count} times</p>
      <JOKER count={count} />
    </div>
  );
}

同样的代码逻辑,如果按照我们的预期点击按钮之后,render,useEffect应该都是执行一次才对。但是严谨的我还是用浏览器执行一下。

从日志上来看,JOKER先用旧的count渲染了一次,然后执行useEffect,然后再用新的count重新渲染。经过一系列的研究...此处省略上万字(刚兴趣的可以留言,我另外写一篇文章分享)...可以理解成这是一个Hooks写法的特性。

但是这里引出另外的一个问题,如果DC里面有两个state,点击按钮事件只是修改另外的一个state的值,那么JOKER也还是会执行一次的。我们不妨把代码改成下面这样,看一下执行之后的日志。

function DC() {
  const [count, setCount] = useState(0);
  const [sum, setSum] = useState(0);
  return (
    <div>
      <button onClick={() => {
        setSum(sum + 1);
        console.log('\n');
      }}>
        Click me
      </button>
      <p>DC: You clicked {count} times</p>
      <p>now this is {sum} times</p>
      <JOKER count={count} />
    </div>
  );
}

执行的结果跟我们猜想是一样的,只要是在Hooks组件里面改变了state的值,整个组件是会重新render的。

看到这里,我们不得不仔细思考一下了,怎么避免这些多余的render导致的性能损耗。如果没有办法解决的话,还不如用回pureComponent,因为当组件多起来的时候,render对内存的消耗肯定是指数上升。

从Hooks的文档我们看到一个useMemo这个API。

这里先简单说一下memoized这个概念。它是JavaScript中的一种通过内存换取耗时的方案。通过缓存结果并在下一个操作中重新使用缓存来加速耗时的操作。如果我们有密集型计算或者密集的CPU操作,我们可以通过将初始操作的结果存在内存里面。操作必然会再次执行,我们将不再麻烦我们的CPU,我们只需要去读取上一次缓存在内存的值。memoize推荐使用lodash实现的,这里对原理做一个简单阐述,不能将代码直接复制线上使用。

function memoize(fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments)
        fn.cache = fn.cache || {};
        return fn.cache[args] ? fn.cache[args] : (fn.cache[args] = fn.apply(this,args))
    }
}

我们这里简单的对这个方案进行一个实验,我们使用著名的斐波那契数列来演示。

function fibonacci(num) {
    if (num == 1 || num == 2) {
        return 1
    }
    return fibonacci(num-1) + fibonacci(num-2)
}

const memFib = memoize(fibonacci)
console.log('profiling tests for fibonacci')
console.time("non-memoized call")
console.log(memFib(6))
console.timeEnd("non-memoized call")
console.time("memoized call")
console.log(memFib(6))
console.timeEnd("memoized call")

从日志上的结果来看,正常的执行时间需要0.23ms,但是用memoize方案只需要0.09ms。就这么简单的一个demo就能做到好几倍的差别,如果真的复杂的计算,性能上的提升会更理想。

回到正题,既然useMemo是提升性能的一个API,那我们应该怎么用?文档上面也没有提供DEMO。也是经过了一些时间的研究跟尝试,找到了正确的一个姿势。

function DC() {
  const [count, setCount] = useState(0);
  const [sum, setSum] = useState(0);
  const memoizedJOKER = useMemo(() => <JOKER count={count} />, [count]);
  return (
    <div>
      <button onClick={() => {
        // setCount(count + 1);
        setSum(sum + 1);
            console.log('---click---');
        console.log('\n');
      }}>
        Click me
      </button>
      <p>DC: You clicked {count} times</p>
      <p>now this is {sum} times</p>
      {memoizedJOKER}
    </div>
  );
}

我们把组件放入useMemo里面,然后在声明好需要监听的值。在这个例子里面,我们把JOKER组件放入了useMemo,并且监听count的值。执行之后是符合我们的预期,JOKER并没有重新执行了。然后如果把setCount这行代码的注释去除,count改变了,JOKER也相应的改变。

看到这里,render的一个机制也就介绍完了,用Hooks的同学真的需要注意一下,当你的组件比较多的时候,嵌套比较深的时候,一不小心其实损耗的内存也是很大的,只是你用的设备好,看起来不差这点内存罢了。最后再插一句,Hooks的出现应该也标记着函数式编程在React日后的版本铁定是潮流跟核心,相关的知识还是要重新补一下。

发布于 2020-01-14

文章被以下专栏收录

    阿里巴巴新零售B系前端团队官方公众号,为您提供高质量的原创、翻译技术文章,以及职业成长、B类业务和阿里文化相关内容。