关于react hooks的性能优化

3,191 阅读9分钟

前言

一、 类组件过渡到函数组件

  1. 类组件以前在组件之间复用状态逻辑很难

react 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。有一些解决此类问题的方案 ,比如render props 和 高阶组件。但会重新组织你的组件结构,会很麻烦,使代码难以理解。而且render props 等其他抽象层组成的组件会形成“嵌套地狱”。所以React 需要为共享状态逻辑提供更好的原生途径。

  1. 复杂组件变得难以理解

组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

3.难以理解的class

除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式

为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。  从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决案。

二、 Hooks

  1. React Hooks 出来很长一段时间了,鉴于使用 react 的结果,给大家分享一些 React Hooks 性能优化。

React 性能优化思路

一、 我觉得 React 性能优化的理念的主要方向就是这两个:

  1. 减少重新 render 的次数。因为在 React 里最重(花时间最长)的一块就是 reconction(简单的可以理解为 diff),如果不 render,就不会 reconction。
  2. 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用。

在使用类组件的时候,使用的 React 优化 API 主要是:shouldComponentUpdate和 PureComponent,这两个 API 所提供的解决思路都是为了减少重新 render 的次数,主要是减少父组件更新而子组件也更新的情况.

但是在函数式组件里面没有声明周期也没有类,那如何来做性能优化呢?

举个例子

现在有个父子组件,子组件依赖父组件传入的 name 属性,但是父组件 name 属性和 text 属性变化都会导致 Parent 函数重新执行,所以即使传入子组件 props 没有任何变化,甚至子组件没有依赖于任何 props 属性,都会导致子组件重新渲染

const Child = ((props: any) => {
  console.log("我更新了...");
  return (
      <div>
          <h3>子组件</h3>
          <div>text:{props.name}</div>
          <div>{new Date().getTime()}</div>
      </div>
  )
})
const Parent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("")
  const handleClick = () => {
      setCount(count + 1);
  }
  const handleInputChange = (e) => {
      setText(e.target.value)
  }
  return (<div>
      <input onChange={handleInputChange} />
      <button onClick={handleClick}>+1</button>
       <div>count:{count}</div>
      <Child name ={text}/>
  </div>)
}

上面的代码执行你会发现,不管是触发 handleInputChange 还是触发 handleClick ,子组件都会在控制台输出 ,"我更新了... " 所以即使传入子组件 props 没有任何变化,甚至子组件没有依赖于任何 props 属性,子组件都会重新渲染

结果如下图:

example_2.png

想要解决重复渲染的问题,可以使用react的亲手制造升级的儿子,他有三个方法用来做优化,分别是 React.memo useCallback useMemo 。

React.memo

使用 memo 包裹子组件时,只有 props 发生改变子组件才会重新渲染。使用 memo 可以提升一定的性能。React.memo在给定相同props的情况下渲染相同的结果,并且通过记忆组件渲染结果的方式来提高组件的性能表现。

const Child = React.memo((props: any) => {
    console.log("我更新了..."); // 只有当props属性改变,即name属性改变时,子组件才会重新渲染
    return (
        <div>
            <h3>子组件</h3>
            <div>text:{props.name}</div>
            <div>{new Date().getTime()}</div>
        </div>
    )
})
const Parent = () => {
    const [count, setCount] = useState(0);
    const [text, setText] = useState("")
    const handleClick = () => {
        setCount(count + 1);
    }
    const handleInputChange = (e) => {
        setText(e.target.value)
    }
    return (<div>
        <input onChange={handleInputChange} />
        <button onClick={handleClick}>+1</button>
         <div>count:{count}</div>
        <Child name ={text}/>
    </div>)
}

此时结果如下:

example_3.png

React.memo 高级用法

默认情况下其只会对 props 的复杂对象做浅层对比(浅层对比就是只会对比前后两次 props 对象引用是否相同,不会对比对象里面的内容是否相同),如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function Child(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 falseChild
  */
   if (prevProps.name === nextProps.name) {
    return true;  // 即子组件不会重新render
  } else {
    return false;
  }
}
export default React.memo(Child, areEqual);

React.memo 原理

export default function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  if (__DEV__) {
    // do something
  }
  return {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
}

memo 方法接受两个参数:

  • type:必选参数,它是一个 React 组件。
  • compare:可选参数,它是一个返回布尔值的比较函数。用于比较前后两个 props 是否相等。

useCallback

但如果传入的 props 包含函数,父组件每次重新渲染都是创建新的函数,所以传递函数子组件还是会重新渲染,即使函数的内容还是一样。如何解决这一问题,我们希望把函数也缓存起来,于是引入 useCallback

useCallback 用于缓存函数,只有当依赖项改变时,函数才会重新执行返回新的函数,对于父组件中的函数作为 props 传递给子组件时,只要父组件数据改变,函数重新执行,作为 props 的函数也会产生新的实例,导致子组件的刷新, 使用 useCallback 可以缓存函数。需要搭配 memo 使用

把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。

const Child = React.memo((props: any) => {
    console.log("我更新了...");
   return (
       <div>
           <h3>子组件</h3>
           <div>text:{props.name}</div>
           <div> <input onChange={props.handleInputChange} /></div>
           <div>{new Date().getTime()}</div>
       </div>
   )
})
const Parent = () => {
   const [count, setCount] = useState(0);
   const [text, setText] = useState("")
   const handleClick = () => {
       setCount(count + 1);
   }
   // 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
   const handleInputChange = useCallback((e) => {
       setText(e.target.value )
   },[])
   return (<div>
       <button onClick={handleClick}>+1</button>
       <div>count:{count}</div>
       <Child name={text} handleInputChange={handleInputChange}/>
   </div>)
}

结果如下:

example_4.png

useCallback 第二个参数依赖项什么情况下使用呢,看下面的例子

//修改handleInputChange
const handleInputChange =useCallback((e) => {
        setText(e.target.value + count)
    },[])

如下图:

example_5.png

count 改变,但 handleInputChange 不依赖与任何项,所以 handleInputChange 只在初始化的时候调用一次函数就被缓存起来,当文本改变时或者 count 改变时函数内部的 count 始终为 0,所以需要将 count 加入到依赖项,count 变化后重新生成新的函数,改变函数内部的 count 值

const handleInputChange =useCallback((e) => {
        setText(e.target.value + count)
    },[count])

useCallback原理

首次挂载组件时,走的时mount**, 组件更新时走的是update**
// mount阶段就是获取到传入的回调函数和依赖数组,保存到hook的memorizedState中,然后返回回调函数。

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

// update阶段

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  // 从hook的memorizedState中获取上次保存的值[callback, deps],
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 比较新的deps和之前的deps是否相等
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果相等,返回memorized的callback
        return prevState[0];
      }
    }
  }
  // 如果deps发生变化,更新hook的memorizedState,并返回最新的callback
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

useMemo

React 的性能优化方向主要是两个:一个是减少重新 render 的次数(或者说减少不必要的渲染),另一个是减少计算的量。

前面介绍的 React.memo 和 useCallback 都是为了减少重新 render 的次数。对于如何减少计算的量,就是 useMemo 来做的

对于如何减少计算的量,就是 useMemo 来做的,useMemo 使用场景请看下面这个例子

const Parent =()=> {
  const [num, setNum] = useState(0);
 
  // 一个非常耗时的一个计算函数
  // result 最后返回的值是 49995000
  
  const expensiveFn=()=> {
    let result = 0;
    
    for (let i = 0; i < 10000; i++) {
      result += i;
    }

    console.log(result) // 49995000
    return result;
  }
 
  const base = expensiveFn();
 
  return (
    <div className="Parent">
      <h1>count:{num}</h1>
      <button onClick={() => setNum(num + base)}>+1</button>
    </div>
  );
}

如下图:

example_6.png

useMemo 做计算结果缓存

针对上面产生的问题,就可以用 useMemo 来缓存 expensiveFn 函数执行后的值。

const computeExpensiveValue=()=> {
  // 计算量很大的代码
  return xxx
}
 
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);

useMemo 的第一个参数就是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为 useMemo 的返回值,第二个参数是一个数组依赖,如果数组里面的值有变化,那么就会重新去执行第一个参数里面的函数,并将函数返回的值缓存起来并作为 useMemo 的返回值 。

了解了 useMemo 的使用方法,然后就可以对上面的例子进行优化,优化代码如下:

const Parent =()=> {
  const [num, setNum] = useState(0);
 
const expensiveFn=()=> {
    let result = 0;
    for (let i = 0; i < 10000; i++) {
      result += i;
    }
    console.log(result)
    return result;
  }
 
  const base = useMemo(expensiveFn, []);
 
  return (
    <div className="Parent">
      <h1>count:{num}</h1>
      <button onClick={() => setNum(num + base)}>+1</button>
    </div>
  );
}

执行上面的代码,然后现在可以观察无论我们点击  +1多少次,只会输出一次 49995000,这就代表 expensiveFn 只执行了一次,达到了我们想要的效果。

example_7.png

useMemo原理

useMemo和useCallback的源码部分比较相似.

// mount阶段, 执行创建函数获得返回值
// 保存到hook的memorizedState中[nextValue, nextDeps]
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
// update阶段
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  // 获取新的deps
  const nextDeps = deps === undefined ? null : deps;
  // 从memorizedState中获得上次保存的值
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      // 比较新deps和旧deps是否相等,如果两者相等,返回旧的创建函数的返回值
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  // 如果deps发生改变,hook中保存新的返回值和deps,并返回新的创建函数的返回值
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

总结

对于性能优化有很多方面:网络、关键路径渲染、打包、图片、缓存等等方面,我只介绍了性能优化中的冰山一角:运行过程中 React 的优化。

React 的优化方向:减少 render 的次数;减少重复计算。

合理的拆分组件其实也是可以做性能优化的,你这么想,如果你整个页面只有一个大的组件,那么当 props 或者 state 变更之后,需要 reconction 的是整个组件,其实你只是变了一个文字,如果你进行了合理的组件拆分,你就可以控制更小粒度的更新。