react传入的组件是underfined_React.memo - React.useCallback - React.useMemo

2875dd272badd758d36dbaf522e0fde2.png

React 核心开发团队一直都致力于提高 React 渲染速度。 React 16 就引入了 React.memo(16.6.0),React.useCallbackReact.useMemo(React Hooks 特性 16.8.0)都是用于优化 React 组件性能。

React.memo

React.memo 一个用于避免组件无效重复渲染的高价组件。与 React.PureComponent 组件和 shouldComponentUpdate() 方法功能类似。但 React.memo 只能用于函数组件,并且如果 React.memo 接受第二个参数 comparecompare 返回值为 true 不渲染,false 则渲染。这与 React.PureComponent 组件和 shouldComponentUpdate() 方法刚好相反。在线实例:

useCallback - CodeSandbox​codesandbox.io
import * as React from 'react';
import { Button, Typography } from 'antd';

const ChildComponent = () => {
  console.log('子组件 ChildComponent');
  return (
    <Button type="primary" ghost>
      子组件 ChildComponent
    </Button>
  );
};

const ParentComponent = () => {
  const [count, setCount] = React.useState < number > 0;
  return (
    <div>
      <Button
        type="primary"
        style={{ marginBottom: 20 }}
        onClick={() => {
          setCount(count + 1);
        }}>
        +1
      </Button>
      <Typography.Paragraph type="danger" strong>
        count:{count}
      </Typography.Paragraph>
      <ChildComponent />
    </div>
  );
};

export default ParentComponent;

运行,每次单击【+1 按钮】,都会导致 ChildComponent 组件重新渲染:

b409d14d8853354205b774884b6eab1e.png

React 渲染机制,ParentComponent 的更新会重新渲染 ChildComponent 组件。如果想用 ChildComponent 组件不渲染,这里可以使用 React.memo 高级组件来优化。在线实例

react-memo - CodeSandbox​codesandbox.io
fd5963db3d7ddfa0f426845be7fc256b.png
const ChildComponent = React.memo(() => {
  console.log('子组件 ChildComponent');
  return (
    <Button type="primary" ghost>
      子组件 ChildComponent
    </Button>
  );
});

使用 React.memoChildComponent 进行包裹:

17795309f2f29dcce33229794e474326.png

从实例可以清楚地知道,ChildComponent 组件被 React.memo 包装后,父组件 ParentComponent 的更新不会引起 ChildComponent 组件重新渲染。

singsong: 如果想更精确控制 React.memo 包裹组件何时更新,可以传入 React.memo 的第二个参数 compare。因为 React.memo 默认只会对 props 做 浅层对比 shallowEqual。
function MyComponent(props) {
  /* 使用 props 渲染 */
}
function compare(prevProps, nextProps) {
  /* 比较 prevProps 与 nextProps */
  // 如果为 true 表示该组件不需要重新渲染,如果为 false 表示重新渲染该组件
}
export default React.memo(MyComponent, compare);
singsong: 如果 React.memo 包裹的函数组件中使用了 useStateuseContext,当 context 与 state 变化时,也会导致该函数组件的更新。

React.memo 关键源码

import { REACT_MEMO_TYPE } from 'shared/ReactSymbols';

export function memo<Props>(type: React$ElementType, compare?: (oldProps: Props, newProps: Props) => boolean) {
  const elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };

  return elementType;
}

何时使用 React.memo

  • 纯函数组件(即相同 props 渲染输出不变)
  • 频繁地重复渲染
  • 传入的 props 基本不变
  • 组件复杂 (渲染开销大)

React.useCallback

在介绍 useCallback 之前,先来看看如下实例的输出:

实例来源​dmitripavlutin.com
function sumFactory() {
  return (a, b) => a + b;
}

const sum1 = sumFactory();
const sum2 = sumFactory();

console.log(sum1 === sum2); // => false
console.log(sum1 === sum1); // => true
console.log(sum2 === sum2); // => true

sumFactory 工厂方法返回的两个方法:sum1sum2 。虽然由同一个工厂方法返回,但两者是完全不同的。

接着再看如下实例:

react-usecallback - CodeSandbox​codesandbox.io
cb46cda899eb0f84a4e76a8e44d98578.png
import * as React from 'react';
import { Button, Typography } from 'antd';

type ChildComponentType = {
  onChildClickCb?: () => void,
};
const ChildComponent: React.FC<ChildComponentType> = React.memo((props: ChildComponentType) => {
  console.log('子组件 ChildComponent');
  return (
    <Button type="primary" ghost>
      子组件 ChildComponent
    </Button>
  );
});

const ParentComponent = () => {
  const [count, setCount] = React.useState < number > 0;
  return (
    <div>
      <Button
        type="primary"
        style={{ marginBottom: 20 }}
        onClick={() => {
          setCount(count + 1);
        }}>
        +1
      </Button>
      <Typography.Paragraph type="danger" strong>
        count:{count}
      </Typography.Paragraph>
      <ChildComponent onChildClickCb={() => {}} />
    </div>
  );
};

export default ParentComponent;

运行,每次单击【+1 按钮】,都会导致 ChildComponent 组件重新渲染:

83af5cb8712a1608cd9436d2159cad20.png

这里可能会有疑问? ,为什么这里 ChildComponent 已使用 React.memo 包裹。怎么 ParentComponent 的更新会导致 ChildComponent 的更新。

问题出在 <ChildComponent onChildClickCb={() => {}} /> 语句中,每次 ParentComponent 更新都会传入新的 onChildClickCb 值。就好比如下实例:

{} === {} // false

这里如果想要传入的 onChildClickCb 值不变,可以使 useCallback 进行包裹。在线实例:

react-usecallback1 - CodeSandbox​codesandbox.io
ef5ac70cdefb094f998ccddd18700fae.png
import * as React from 'react';
import { Button, Typography } from 'antd';

type ChildComponentType = {
  onChildClickCb?: () => void,
};
const ChildComponent: React.FC<ChildComponentType> = React.memo((props: ChildComponentType) => {
  console.log('子组件 ChildComponent');
  return (
    <Button type="primary" ghost>
      子组件 ChildComponent
    </Button>
  );
});

const ParentComponent = () => {
  const [count, setCount] = React.useState < number > 0;
  const onChildClickCb = React.useCallback(() => {}, []); // 使用 useCallback 包裹 onChildClickCb
  return (
    <div>
      <Button
        type="primary"
        style={{ marginBottom: 20 }}
        onClick={() => {
          setCount(count + 1);
        }}>
        +1
      </Button>
      <Typography.Paragraph type="danger" strong>
        count:{count}
      </Typography.Paragraph>
      <ChildComponent onChildClickCb={onChildClickCb} />
    </div>
  );
};

export default ParentComponent;

776ff0b9299ed89e1f60e9ae280f3a5e.png

React.useMemo

React.useMemoReact.useCallback 函数签名类似。唯一不同的是 React.useMemo 缓存第一个参数的返回值(nextCreate()),而 React.useCallback 缓存第一个参数的函数(callback)。 因此 React.useMemo 常用于缓存计算量密集的函数返回值。

关键源码​github.com
export function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();

  const nextInputs = inputs !== undefined && inputs !== null ? inputs : [callback];

  const prevState = workInProgressHook.memoizedState;
  if (prevState !== null) {
    const prevInputs = prevState[1];
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      return prevState[0];
    }
  }
  workInProgressHook.memoizedState = [callback, nextInputs];
  return callback;
}

export function useMemo<T>(nextCreate: () => T, inputs: Array<mixed> | void | null): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();

  const nextInputs = inputs !== undefined && inputs !== null ? inputs : [nextCreate];

  const prevState = workInProgressHook.memoizedState;
  if (prevState !== null) {
    const prevInputs = prevState[1];
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      return prevState[0];
    }
  }

  const nextValue = nextCreate(); // 计算值
  workInProgressHook.memoizedState = [nextValue, nextInputs];
  return nextValue;
}

workInProgressHook 对象

{
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

areHookInputsEqual() 源码

export default function areHookInputsEqual(arr1: any[], arr2: any[]) {
  for (let i = 0; i < arr1.length; i++) {
    const val1 = arr1[i];
    const val2 = arr2[i];
    if (
      (val1 === val2 && (val1 !== 0 || 1 / val1 === 1 / (val2: any))) ||
      (val1 !== val1 && val2 !== val2) // eslint-disable-line no-self-compare
    ) {
      continue;
    }
    return false;
  }
  return true;
}

从源码可以清楚了解到,只会记录上一次的记录。对比算法也是浅层对比。

了解 React.useMemo 工作原理,来实例实践一下:

react-usememo - CodeSandbox​codesandbox.io
8ddca52c9f31f785e7600394ef6ec247.png
import * as React from 'react';
import { Button, Typography } from 'antd';

type ChildComponentType = {
  resultComputed?: number[],
};

const ChildComponent: React.FC<ChildComponentType> = React.memo((props: ChildComponentType) => {
  console.log('子组件 ChildComponent', props.resultComputed);
  return (
    <Button type="primary" ghost>
      子组件 ChildComponent
    </Button>
  );
});

const ParentComponent = () => {
  const [count, setCount] = React.useState < number > 0;
  const calculator = (num?: number) => {
    return [];
  };
  const resultComputed = calculator();

  return (
    <div>
      <Button
        type="primary"
        style={{ marginBottom: 20 }}
        onClick={() => {
          setCount(count + 1);
        }}>
        +1
      </Button>
      <Typography.Paragraph type="danger" strong>
        count:{count}
      </Typography.Paragraph>
      <ChildComponent resultComputed={resultComputed} />
    </div>
  );
};

export default ParentComponent;

运行,每次单击【+1 按钮】,都会导致 ChildComponent 组件重新渲染:

bf139d83c77623fc72b008bcdcb23d6f.png

问题出在如下代码:

const calculator = (num?: number) => {
  return [];
};
const resultComputed = calculator();

每次调用 calculator() 都返回新的 resultComputed。如果要修复这个问题,这里可以使用 React.useMemocalculator 方法进行包裹。

在线实例​codesandbox.io
const resultComputed = React.useMemo<number[]>(calculator, []);

运行,每次单击【+1 按钮】

60d1468078192405851fa3bcc8cc3e7a.png

总结

本文是作者最近学习的一点心得,与大家分享分享。不过不要 "因为有,强行使用"。只有在发现页面卡顿时,或者性能不好时,可以从这方面入手。原本 React 重新渲染对性能影响一般情况可以忽略不计。因为 memoized 还是需要消耗一定内存的,如果你不正确地大量使用这些优化,可能适得其反哦 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值