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

react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第1张图片

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 (
    
  );
};

const ParentComponent = () => {
      
  const [count, setCount] = React.useState < number > 0;
  return (
    
count:{ count}
); }; export default ParentComponent;

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

react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第2张图片

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

react-memo - CodeSandbox​codesandbox.io
react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第3张图片
const ChildComponent = React.memo(() => {
      
  console.log('子组件 ChildComponent');
  return (
    
  );
});

使用 React.memoChildComponent 进行包裹:

react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第4张图片

从实例可以清楚地知道,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(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
react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第5张图片
import * as React from 'react';
import {
       Button, Typography } from 'antd';

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

const ParentComponent = () => {
      
  const [count, setCount] = React.useState < number > 0;
  return (
    
count:{ count} {}} />
); }; export default ParentComponent;

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

react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第6张图片

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

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

{} === {} // false

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

react-usecallback1 - CodeSandbox​codesandbox.io
react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第7张图片
import * as React from 'react';
import {
       Button, Typography } from 'antd';

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

const ParentComponent = () => {
      
  const [count, setCount] = React.useState < number > 0;
  const onChildClickCb = React.useCallback(() => {}, []); // 使用 useCallback 包裹 onChildClickCb
  return (
    
count:{ count}
); }; export default ParentComponent;

react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第8张图片

React.useMemo

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

关键源码​github.com
export function useCallback(callback: T, inputs: Array | 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(nextCreate: () => T, inputs: Array | 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
react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第9张图片
import * as React from 'react';
import {
       Button, Typography } from 'antd';

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

const ChildComponent: React.FC = React.memo((props: ChildComponentType) => {
      
  console.log('子组件 ChildComponent', props.resultComputed);
  return (
    
  );
});

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

  return (
    
count:{ count}
); }; export default ParentComponent;

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

react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第10张图片

问题出在如下代码:

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

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

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

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

react传入的组件是underfined_React.memo - React.useCallback - React.useMemo_第11张图片

总结

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

你可能感兴趣的:(react传入的组件是underfined_React.memo - React.useCallback - React.useMemo)