React 之 re-render的原理和优化

今天看到一篇文章,关于react re-render的原理,这里记录并学习一下

对于函数组件的re-render,大致分为一下三种情况:

  • 组件本身使用useState 或者 useReducer 更新,引起的re-render;
  • 父组件更新引起子组件的re-render;
  • 组件本身使用了useContext,context更新引起的re-render;

1:组件本身使用useState 或者 useReducer 更新,引起的re-render

        1.1 常规使用

以基数组件为例,每次点击add,都会打印"re-render",说明引起了re-render

import React, { useState } from 'react';
import './index.css';
export default function Counter() {
  console.log('re-render');

  const [count, addCount] = useState(0);
  return (
    <div className="counter">
      <div className="counter-num">{count}</div>
      <button
        onClick={() => {
          addCount(count + 1);
        }}
      >
        add
      </button>
    </div>
  );
}

1.2 immutation state

 将上面的计数组件的state值改为引用类型,发现点击并不会引起re-render

import React, { useState } from 'react';
import './index.css';

export default function index() {
  console.log('counter render');

  const [count, addCount] = useState({ num: 0, time: Date.now() });
  const clickHandler = () => {
    count.num++;
    count.time = Date.now();
    addCount(count);
  };
  return (
    <div className="counter">
      <div className="counter-num">
        {count.num}, {count.time}
      </div>
      <button onClick={clickHandler}>add</button>
    </div>
  );
}

 这里能看到,只有在挂载阶段打印了re-render

再改一改:  setState的时候改用参数形式,也会引发re-render

import React, { useState } from 'react';
import './index.css';

export default function index() {
  console.log('counter render');

  const [count, addCount] = useState({ num: 0, time: Date.now() });
  const clickHandler = () => {
    addCount((param) => ({ num: param.num + 1, time: Date.now() }));
  };
  return (
    <div className="counter">
      <div className="counter-num">
        {count.num}, {count.time}
      </div>
      <button onClick={clickHandler}>add</button>
    </div>
  );
}

真实的原因在于,更新 state 的时候,会有一个新老 state 的比较,用的是 Object.is 进行比较,如果为 true 则直接返回不更新,源码如下(objectIs 会先判断 Object.is 是否支持,如果不支持则重新实现,eagerState 就是 oldState ):

if (objectIs(eagerState, currentState)) {
  return;
}

所以更新 state 时候要注意,state 为不可变数据,每次需要一个新地址的值才会触发更新。

2:父组件更新引起子组件re-render 

2.1、常规使用

现在稍微改造上面计数的组件,添加一个子组件 Hello,如下点击会发现,每次都会输出 "hello render",也就是说,每次更新都引起了 Hello 的 re-render,但是其实 Hello 组件的属性根本就没有改变:

import React, { useState } from 'react';
import './index.css';

export default function index() {
  const Hello = ({ name }) => {
    console.log('hello render');
    return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
  };
  console.log('counter render');
  const [count, addCount] = useState({ num: 0, time: Date.now() });
  const clickHandler = () => {
    addCount((param) => ({ num: param.num + 1, time: Date.now() }));
  };
  return (
    <div className="counter">
      <Hello name="react" />
      <br />
      <div className="counter-num">
        {count.num}, {count.time}
      </div>
      <button onClick={clickHandler}>add</button>
    </div>
  );
}

 对于这种不必要的re-reder,是有手段进行优化的

2.2、优化组件设计

2.2.1、将更新部分抽离成单独组件

import React, { useState } from 'react';
import './index.css';

export default function index() {
  const Hello = ({ name }) => {
    console.log('hello render');
    return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
  };
  return (
    <div className="counter">
      <Hello name="react" />
      <Counter />
    </div>
  );
}

// 单独抽离成组件
const Counter = () => {
  console.log('counter render');
  const [count, addCount] = useState({ num: 0, time: Date.now() });
  const clickHandler = () => {
    addCount((param) => ({ num: param.num + 1, time: Date.now() }));
  };
  return (
    <>
      <div className="counter-num">
        {count.num}, {count.time}
      </div>
      <button onClick={clickHandler}>add</button>
    </>
  );
};

 2.3、React.memo

对于是否需要 re-render,类组件提供了两种方法:PureComponent 组件和 shouldComponentUpdate 生命周期方法。

对于函数组件来说,有一个 React.memo 方法,可以用来决定是否需要 re-render,如下我们将 Hello 组件 memo 化,这样点击更新数字的时候, Hello 组件是不会 re-render 的。除非 Hello 组件的 props 更新:

import React, { useState } from 'react';
import './index.css';

//React.memo
const Hello = React.memo(({ name }) => {
  console.log('hello render');
  return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
});

export default function index() {
  const [count, addCount] = useState(0);
  console.log('counter render');

  const clickHandler = () => {
    addCount(count + 1);
  };
  return (
    <div className="counter">
      <Hello name="react" />
      <div className="counter-num">{count}</div>
      <button onClick={clickHandler}>add</button>
    </div>
  );
}

 memo 方法的源码定义简略如下:

exportfunction memo<Props>(
  type: React$ElementType, // react 自定义组件
  compare?: (oldProps: Props, newProps: Props) => boolean, // 可选的比对函数,决定是否 re-render
) {
    ...
    const elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
    ...
  
  return elementType;
}

memo 的关键比对逻辑如下,如果有传入 compare 函数则使用 compare 函数决定是否需要 re-render,否则使用浅比较 shallowEqual 决定是否需要 re-render:

var compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;

if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

既然默认不传 compare 时,用的是浅对比,那么对于引用类的 props,就要注意了,尤其是事件处理的函数,如下,我们给 Hello 组件添加一个点击事件,这时我们发现每次点击计数,Hello 组件又开始 re-render 了:

import React, { useState } from 'react';
import './index.css';

//React.memo
const Hello = React.memo(({ name }) => {
  console.log('hello render');
  return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
});

export default function index() {
  const [count, addCount] = useState(0);
  console.log('counter render');

  const clickHandler = () => {
    addCount(count + 1);
  };
  // Hello组件添加点击事件
  const helloClick = () => {
    console.log('Hello组件点击回调~~~');
  };

  return (
    <div className="counter">
      <Hello name="react" onClick={helloClick} />
      <div className="counter-num">{count}</div>
      <button onClick={clickHandler}>add</button>
    </div>
  );
}

 这是因为每次点击计数,都会重新定义 clickHandler 处理函数,这样 shallowEqual 浅比较发现 onClick 属性值不同,于是将会进行 re-render。

2.3.1、useCallback

在上面的基础上,我们阔以用useCallback将定义的函数缓存起来,也不会触发re-render

import React, { useState } from 'react';
import './index.css';

//React.memo
const Hello = React.memo(({ name }) => {
  console.log('hello render');
  return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
});

export default function index() {
  const [count, addCount] = useState(0);
  console.log('counter render');

  const clickHandler = () => {
    addCount(count + 1);
  };
  // 使用useCallback缓存
  const helloClick = React.useCallback(() => {
    console.log('Hello组件点击回调~~~');
  }, []);

  return (
    <div className="counter">
      <Hello name="react" onClick={helloClick} />
      <div className="counter-num">{count}</div>
      <button onClick={clickHandler}>add</button>
    </div>
  );
}

useCallback 的原理主要是在挂载的时候,将定义的 callback 函数及 deps 依赖挂载该 hook 的 memoizedState,当更新时,将依赖进行对比,如果依赖没变,则直接返回老的 callback 函数,否则则更新新的 callback 函数及依赖:

// 挂载时
function mountCallback(callback, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

// 更新时
function updateCallback(callback, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];

      // 如果依赖未变,则直接返回老的函数
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  // 否则更新新的 callback 函数
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

 看起来好像是没问题了,但是如果我们在刚才 callback 函数中使用了 count 这个 state 值呢?

  // 使用useCallback缓存 并且使用count
  const helloClick = React.useCallback(() => {
    console.log('Hello组件点击回调~~~', 'count: ', count);
  }, [count]);

当我们点击了几次计数,然后再点击 Hello 组件时,会发现我们打印的 count 还是挂载时候的值,而不是最新的 count 值。其实,这都是是闭包惹得祸(具体解释可参考:Be Aware of Stale Closures when Using React Hooks)。所以为了让 callback 函数中可以使用最新的 state,我们还要将该 state 放入 deps 依赖,但是这样依赖更新了,callback 函数也将会更新,于是 Hello 组件又将会 re-render,这又回到了从前。

 这样我们得出了一个结论:

当 callback 函数需要使用 state 值时,如果是 state 值更新引起的更新,useCallback 其实是没有任何效果的。

 其实react底层已经做了一些性能优化处理,就比如批量更新等等,但是我们在使用的时候也得注意下这些问题,

React和vue的另一个突出区别就是: vue底层已经最大限度的把性能优化给做了,不需要程序员去考虑性能问题,而React有一部分性能优化是留给了程序员,有没有这个感觉?

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

superTiger_y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值