Hook 概述

Hook是React 16.8的新增性,它可以让我们在不编写class的情况下使用state及其他React特性。Hook的诞生是为共享状态逻辑提供更好的原生途径。

我们在项目中也全面推广使用,一个很直观的感受,写起来很爽很简洁。通常情况下,我们会习惯把页面切割成很多的组件(function component),这让我们很容易的组织和管理页面的组成。但在function component中,重新渲染(re-render)很轻易的就会被触发,少量的组件还不会造成太大的问题,但是,遇到复杂的业务,大量的组件不断的被重新渲染,就会给浏览器很大的负担,会造成用户的体验不佳。

所以,我们有必要关注下使用React Hooks函数式组件的优化手段。

优化策略

React Hook优化的三个策略:

  1. 避免组件重新渲染
  2. 减少不必要的计算量
  3. 合理的State/Props数据

原则:

  1. 缓存组件
  2. 缓存计算
  3. 合理数据控制

优化方式:memo、useMemo、useCallback

对于开发React的同学来说,应该比较熟悉。这三种方法都是React官方提供的提供的用来减少不必要计算和渲染的函数工具。

memo

我们经常会让子组件依赖父组件的状态(state)和事件(event),在父组件中定义状态和事件方法,利用props将两者传递到子组件中。

如果父组件的状态改变,但是props的结果没有变,子组件仍然会被重新渲染,但是子组件的结果没有变化,多余的渲染造成性能上的浪费。

所以,React提供了 React.memo 来帮助我们解决这个问题:

  1. const MyComponent = React.memo(Component = (props) => {
  2. /* render using props */
  3. });

React.memo 是以HOC(higher order component)的方式使用,我们只需要在需要减少渲染的组件外边再包裹一层 React.memo ,就可以让React帮我们记住原本的props。

Example

  1. import React from "react";
  2. const MyComponent = ({ myprops }) => {
  3. const refCount = React.useRef(0);
  4. refCount.current++;
  5. return (
  6. <p>
  7. {myprops}, Ref Count: {refCount.current}
  8. </p>
  9. );
  10. };
  11. const MemorizeMyComponent = React.memo(MyComponent);
  12. const MemoDemo = () => {
  13. const [state, setState] = React.useState("");
  14. const handleSetState = (e) => {
  15. setState(e.target.value);
  16. };
  17. return (
  18. <div className="App">
  19. <input type="text" value={state} onChange={handleSetState} />
  20. <MyComponent myprops="MyComponent" />
  21. <MemorizeMyComponent myprops="MemorizeMyComponent" />
  22. </div>
  23. );
  24. };
  25. export default MemoDemo;

在示例程序中,我们改变父组件的state值,使用momo的组件在props没有变化的情况下,是没有重新渲染的,refCount值也没有变化。反之,没有使用memo的组件,会触发子组件的重新渲染,并强迫refCount不断增加。

然而,React.memo 是用 shallowly compare 的方法确认props的值是否一样,shallowly compare 在props 是Number或者String时比较的是数值,当props是Object时,比较的是记忆体位置(reference)。

因此,当父组件重新渲染时,在父组件宣告的Object都会重新分配记忆体位置,所以想要利用React.memo 防止重新渲染就会失效。

要解决这个问题的方法有两种:

  1. React.memo 提供了第二个参数,让我们自定比较props的方法,让Object不再是比较记忆体位置:

    1. function MyComponent(props) {
    2. /* render using props */
    3. }
    4. function areEqual(prevProps, nextProps) {
    5. /*
    6. return true if passing nextProps to render would return
    7. the same result as passing prevProps to render,
    8. otherwise return false
    9. */
    10. }
    11. export default React.memo(MyComponent, areEqual);
  2. 使用React.useCallback,让React可以自动记住Object的记忆体位置,解决shallowly compare的比较问题。

    小结

  3. HOC组件,需要一层函数包裹

  4. Memo能够缓存上一次渲染结果,依据是控制Props是否产生变化
  5. 如果Props不是Shallowly Compare,那么需要使用的第二个函数参数控制是否需要重新渲染
  6. 更好的方式解决Shallowly Compare的问题?

useCallback

当父组件传递的props是Object时,父组件的状态被改变触发重新渲染,Object的记忆体位置也会被重新分配。React.memo 会用shallowly compare 比较props 中Object的记忆体位置,这个比较会让子组件重新被渲染。

因此,React提供了React.useCallback这个方法让React在父组件重新渲染时,如果 dependencies array 中的值没有在被修改的情况下,它会帮助我们记住Object,防止Object被重新分配记忆体位置。

  1. const memoizedCallback = useCallback(
  2. () => {
  3. doSomething(a, b);
  4. },
  5. [a, b],
  6. );

所以,当React.useCallback能够记住Object的记忆体位置,就可以避免父组件重新渲染后,Object被重新分配记忆体位置,造成 React.memo 的 shallowly compare 发现传递的Object记忆体位置不同。

Example

同样的,当我们在父组件改变input绑定的状态触发重新渲染,使用 useCallback 记住记忆体未知的function 就没有让子组件重新渲染,也咩有让refCount 增加;反之,没有被记住记忆体位置的function会被重新渲染。

  1. import React from "react";
  2. const MyComponent = React.memo((props) => {
  3. const refCount = React.useRef(0);
  4. refCount.current++;
  5. const callback = props.useCallback ? "useCallback" : "without useCallback";
  6. return (
  7. <p>
  8. MyComponent {callback}. Ref Count: {refCount.current}.
  9. </p>
  10. );
  11. });
  12. const equals = (a, b) => {
  13. return a === b ? "Equal" : "Different";
  14. };
  15. const UseCallbackMemo = () => {
  16. const [state, setState] = React.useState("");
  17. const [someArg, setSomeArg] = React.useState("argument");
  18. const handleSomethingUseCallback = React.useCallback(() => {}, [someArg]);
  19. const handleSomething = (e) => {
  20. setState(e.target.value);
  21. };
  22. const handleSetState = (e) => {
  23. setState(e.target.value);
  24. };
  25. const refHandleSomethingUseCallback = React.useRef(
  26. handleSomethingUseCallback
  27. );
  28. const refHandleSomething = React.useRef(handleSomething);
  29. return (
  30. <div className="App">
  31. <input type="text" value={state} onChange={handleSetState} />
  32. <p>
  33. handleSomethingUseCallback:{" "}
  34. {equals(
  35. refHandleSomethingUseCallback.current,
  36. handleSomethingUseCallback
  37. )}
  38. </p>
  39. <p>
  40. handleSomething: {equals(refHandleSomething.current, handleSomething)}
  41. </p>
  42. <MyComponent
  43. handleSomething={handleSomethingUseCallback}
  44. useCallback={true}
  45. />
  46. <MyComponent handleSomething={handleSomething} useCallback={false} />
  47. </div>
  48. );
  49. };
  50. export default UseCallbackMemo;

练习

接下来,我们使用上面已有的内容,来优化这个很普通的Hook函数组件:

  1. import React from "react";
  2. const Button = ({ handleClick }) => {
  3. const refCount = React.useRef(0);
  4. return (
  5. <button onClick={handleClick}>
  6. button render count: {refCount.current++}
  7. </button>
  8. );
  9. };
  10. const UseCallbackTest = () => {
  11. const [isOn, setIsOn] = React.useState(false);
  12. const handleClick = () => setIsOn(!isOn);
  13. return (
  14. <div className="App">
  15. <h2>{isOn ? "on" : "off"}</h2>
  16. <Button handleClick={handleClick}></Button>
  17. </div>
  18. );
  19. };
  20. export default UseCallbackTest;
  1. 首先,发现当前组件的问题?
  2. 提出解决方案?并进一步进行优化。

    Note:function component当中没有this,所有component只能通过闭包去取外部的变量,而state在React render的机制里面是 imumutable, 所以funciton还是会使用一开始useState回传的isOn,而非rerender后的isOn,所以就会造成打开后就关闭不了了。 如果对state熟悉,应该知道setState除了接收setState之外,还接收updater function,这个function input是prevState会return下个state。 同样的,updater function的机制在hook中,同样保留,因此我们可以使用update function取得上一个state而不是用closure的方式去取得外部状态。

分析

useCallback源码实现跟useMemo基本上完全一模一样,不同的是useMemo会调用函数获取缓存的值,而useCallck保存的函数所以不需要调用。其余代码一模一样,这里就不做赘述了直接贴源码:
mountCallback
updateCallback
areHookInputsEqual

  1. function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  2. const hook = mountWorkInProgressHook();
  3. const nextDeps = deps === undefined ? null : deps;
  4. //useCallback缓存的是函数 直接保存的就是函数引用
  5. hook.memoizedState = [callback, nextDeps];
  6. return callback;
  7. }
  8. function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  9. const hook = updateWorkInProgressHook();
  10. const nextDeps = deps === undefined ? null : deps;
  11. const prevState = hook.memoizedState;
  12. if (prevState !== null) {
  13. if (nextDeps !== null) {
  14. const prevDeps: Array<mixed> | null = prevState[1];
  15. if (areHookInputsEqual(nextDeps, prevDeps)) {
  16. return prevState[0];
  17. }
  18. }
  19. }
  20. hook.memoizedState = [callback, nextDeps];
  21. return callback;
  22. }

小结

  1. useCallback返回Memoized Callback
    1. 两种状态:mount/update
    2. fiberNode链表记录缓存值
    3. update阶段shallowly compare对比是否更新
  2. 如果依赖数组的值没有产生变化,那么不会产生新的Function Reference
  3. 通过Updater Function获取最新的State
  4. 配合memo一起使用

    useMemo

    有一种情况,和父组件没有关系,当组件重新渲染时,当前组件内部的function和运行都会重新计算,也会造成很大的负担,因此,这种问题也是不熟悉hook的同学经常犯的问题,这个也是性能优化的一个点。

所以,当传入的依赖或引用未变化时,那么返回的state引用还是一样的,这样child就不会重新渲染。

React.useMemo 是让React记住函数返回的值,如果 dependenices array 中的变量没有被修改,React.memo 会沿用上一次返回的值。

  1. const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Example

当我们的父组件改变input绑定的值后触发重新渲染,使用useMemo记住的值的子组件没有重新渲染,并让refCount增加;反之没有被记住的值的子组件一直在触发重新渲染并计算。

  1. import React from "react";
  2. const MyComponentWithoutUseMemo = () => {
  3. const refCount = React.useRef(0);
  4. const myfunction = () => {
  5. refCount.current++;
  6. return 1;
  7. };
  8. const value = myfunction();
  9. return <p>MyComponent without useMemo. Ref count: {refCount.current}</p>;
  10. };
  11. const MyComponent = () => {
  12. const refCount = React.useRef(0);
  13. const myfunction = () => {
  14. refCount.current++;
  15. return 1;
  16. };
  17. const value = React.useMemo(() => {
  18. return myfunction();
  19. }, []);
  20. return <p>MyComponent useMemo. Ref count: {refCount.current}</p>;
  21. };
  22. const MyComponentMemo = ({ value }) => {
  23. return <p>MyComponent useMemo. {value}</p>;
  24. };
  25. const UseMemoDemo = () => {
  26. const [state, setState] = React.useState("");
  27. const [val, setVal] = React.useState(Date.now());
  28. const handleSetState = (e) => {
  29. setState(e.target.value);
  30. };
  31. const newValue = "Value:" + val + Math.ceil(Math.random() * 1000);
  32. return (
  33. <div className="App">
  34. <input type="text" value={state} onChange={handleSetState} />
  35. <MyComponentWithoutUseMemo />
  36. <MyComponent />
  37. <MyComponentMemo value={newValue} />
  38. <button
  39. onClick={() => {
  40. setVal(Date.now());
  41. }}
  42. >
  43. change val
  44. </button>
  45. </div>
  46. );
  47. };
  48. export default UseMemoDemo;

Note:React官网提醒You may rely on **useMemo** as a performance optimization, not as a semantic guarantee。因此,不要什么都丢到useMemo里面去,在需要优化时才引用。否则只是让React处理更多的事情,造成更大的负担。

分析

从源码可以看出,react把所有的hooks分成了两个阶段的hooks:

  • mount阶段对应第一次渲染初始化时候调用的hooks方法,分别对应了mountMemo,mountCallback以及其他hooks。
  • update阶段对应setXXX函数触发更新重新渲染的更新阶段,分别对应了updateMemo,updateCallback以及其他hooks ```javascript // react-reconciler/src/ReactFiberHooks.js // Mount 阶段Hooks的定义 const HooksDispatcherOnMount: Dispatcher = { useCallback: mountCallback, useMemo: mountMemo, // 其他Hooks };

// Update阶段Hooks的定义 const HooksDispatcherOnUpdate: Dispatcher = { useCallback: updateCallback, useMemo: updateMemo, // 其他Hooks };

  1. hooksmount阶段和update阶段所调用的逻辑是不一样的。
  2. <a name="J5mEU"></a>
  3. ##### mountMemo
  4. 1. 跟其他hook一样,在mount阶段直接创建一个hook挂载到链表上。
  5. 1. mount初始化的时候直接调用传入的函数获取需要缓存的值然后直接返回,这样子我们在const state = useMemo(() => {val: 1}, [val])就可以拿到相应的值
  6. 1. useState不一样的是:useState是直接保存值到hookmemoizedState属性上,useMemo保存的是一个长度为2的数组,值分别是上面调用后的值以及传入的依赖
  7. ```javascript
  8. function mountMemo<T>(
  9. nextCreate: () => T,
  10. deps: Array<mixed> | void | null,
  11. ): T {
  12. // 创建hook对象拼接在hook链表上
  13. const hook = mountWorkInProgressHook();
  14. const nextDeps = deps === undefined ? null : deps;
  15. // 调用我们传入的函数 获取需要缓存的值
  16. const nextValue = nextCreate();
  17. //与useState直接保存值的不同 useMemo保存在memoizedState的是一个数组
  18. // 第一个值是需要缓存的值 第二个是传入的依赖
  19. hook.memoizedState = [nextValue, nextDeps];
  20. return nextValue;
  21. }

updateMemo
  • 跟useState一样通过updateWorkInProgressHook获取更新时当前的useMemo的hook对象。
  • 如果上一次的useMemo值(memoizedState)不为空并且这一次传入的依赖(nextDeps)不为空,那么两次依赖做浅比较。
  • 依赖没有发生变化,那么直接返回数组第一个值即上一次渲染的值。如果依赖发生变化重新调用函数生成新的值. ```javascript function updateMemo( nextCreate: () => T, deps: Array | void | null, ): T { // 第一篇已经说过 获取相对应的hook对象 const hook = updateWorkInProgressHook(); // 获取更新时的依赖 const nextDeps = deps === undefined ? null : deps; // 获取上一次的数组值 const prevState = hook.memoizedState; if (prevState !== null) { // Assume these are defined. If they’re not, areHookInputsEqual will warn. // 如果这次传入的依赖不为空做浅比较 如果依赖没有发生变化那么直接返回上一次的值 if (nextDeps !== null) {
    1. const prevDeps: Array<mixed> | null = prevState[1];
    2. if (areHookInputsEqual(nextDeps, prevDeps)) {
    3. return prevState[0];
    4. }
    } } // 依赖发生变化 重新调用生成新的值 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }

// 浅比较dep的函数 function areHookInputsEqual( nextDeps: Array, prevDeps: Array | null, ) { if (prevDeps === null) { return false; }

// 浅比较 for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; } ```

小结

  1. useMemo返回Memoized Value
  2. 如果依赖数组的值没有产生变化,那么不会产生新的Value
  3. 作为组件自身优化的一种方式,减少计算量
  4. 类似Vue的Computed,但不建议作为语义化使用
  5. 源码和useCallback一模一样,唯一的区别就是一个获取缓存的函数,一个获取缓存值

    数据控制

  6. 优化state:不是所有状态都应该放在组件的 state 中,例如缓存数据、常量、非UI状态数据

  7. 优化props:单一原则、影响shallowly compare、提升缓存命中率
  8. 不要滥用 Context
    1. 依赖context的组件具有穿透性
    2. 状态作用域, 全局
    3. 只放置必要的,关键的,被大多数组件所共享的状态

总结

  1. memo和useCallback通过组合技巧达到较少渲染的效果
  2. memo能够侦测props有没有产生变化,减少不必要渲染
  3. useCallback能够让props的object在父组件重新渲染时,不被重新分配地址
  4. useMemo能够让组件重新渲染时,避免不必要的重复运算
  5. 使用传入的状态,尽量不要使用闭包中的 State
  6. 管理复杂的状态可以考虑使用useReducer等类似的方式,对状态操作定义类型,执行不同的操作
  7. 优化State/Props数据、小心使用Contenxt

结束

Stay Hungry, Stay Foolish
解释,不同思考角度:
求知若饥,虚心若愚
做个吃货,做个二货