Hooks 的性能优化及可能会遇到的坑总结

组件 PureRender

class 组件中性能优化能够经过 shouldComponentUpdate 实现或者继承自 PureComponent,固然后者也是经过 shouldComponentUpdate 去作的,内部对 stateprops 进行了 shallowEqual。数组

对于函数组件来讲并无这个生命周期能够调用,所以想实现性能优化只能经过 React.memo(<Component />) 来作,这种作法和继承 PureComponent 的原理一致。性能优化

另外若是你的函数组件须要拿到它的 ref,可使用如下工具函数:微信

function memoForwardRef<N, P>(comp: RefForwardingComponent<N, P>) {
  return memo(forwardRef<N, P>(comp));
}
复制代码

可是并非以上作法之后性能就万事大吉了,你还得保证传递的 props 以及内部的状态的引用不发生预期以外的变化。闭包

保持局部不变

对于函数组件来讲,变量的引用是须要重点关注的问题,不管是函数亦或者对象。函数

const Child = React.memo(({ columns }) => {
  return <Table columns={columns} />
})
const Parent = () => {
  const data = [];
  return <Child columns={data} />
}
复制代码

对于以上组件来讲,每次 Parent 渲染的时候虽然 columns 内容没有变,可是 columns 的引用已经变了。当 props 传递给 Child 的时候,即便使用了 React.memo 可是性能优化也失效了。工具

对于这种状况,能够经过 useMemo 将引用存储起来,依赖不变引用也就不变。性能

const data = useMemo(() => [], [])
复制代码

useMemo 的场景可能是用于值的计算。好比密集型计算场景下你确定不但愿组件从新渲染的时候,依赖项没有变动缺重复执行计算函数获得相同的值。优化

对于函数来讲,若是你想保存它的引用的话可使用 useCallback 来作。ui

function Counter() {
  const [count, setCount] = useState(0)


  // 这样写函数,每次从新渲染都会再次建立一个新的函数
  const onIncrement = () => {
    setCount(count => count + 1)
  }

  const onIncrement = useCallback(() => {
    setCount(count => count + 1)
  }, [])

  return (
    <div> <button onClick={onIncrement}>INCREMENT</button> <p>{count}</p> </div>
  )
}
复制代码

对于以上代码来讲,组件每次渲染的时候使用了 useCallback 包裹的 onIncrement 函数引用不会改变,这也就意味着不须要频繁建立及销毁函数了。spa

可是在 useCallback 存在依赖的状况下函数引用并不必定按照你的想法正常保持不变,好比以下案例:

function Counter() {
  const [count, setCount] = useState(0)

  const onIncrement = useCallback(() => {
    setCount(count => count + 1)
  }, [])
  
  const onLog = useCallback(() => {
    console.log(count)
  }, [count])

  return (
    <div> <button onClick={onIncrement}>INCREMENT</button> <button onClick={onLog}>Log</button> <p>{count}</p> </div>
  )
}
复制代码

count 每次改变形成组件从新渲染的时候,onLog 函数都会从新建立一次。两种常规方法能够保持在这种状况下函数引用不被改变。

  1. 使用 useEventCallback
  2. 使用 useReducer
function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}
复制代码

useEventCallback 使用了 ref 不变的特性,保证回调函数的引用永远不变。另外在 Hooks 中,dispatch 也是不变的,因此把依赖 ref 改为 dispatch,而后在回调中调用 dispatch 就是另外一种作法了。

性能优化并非银弹

凡事都有两面性,在引入以上这些性能优化的时候你已经下降了本来的性能,毕竟它们都是有使用代价的,咱们能够来阅读下 useCallbackuseMemo 的核心源码:

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateMemo(nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
复制代码

上述源码实现思路大体是从 fiber 中取出 memoizedState,而后对比先后 Deps,对比的实现也采用了 shallowEqual,最后若是有变化的话就重置 memoizedState

能够看出来,本文中讲到的性能优化方案基本都是采用了 shallowEqual 来对比先后差别,因此不必为了性能优化而优化。

Hooks 的坑

Hooks 的坑 99% 都是闭包引发的,咱们经过一个例子来了解下什么状况下会由于闭包致使问题。

function App() {
  const [state, setState] = React.useState(0)
  // 连点三次你以为答案会是什么?
  const handleClick = () => {
    setState(state + 1)
    setTimeout(() => {
      console.log(state)
    }, 2000)
  }

  return (
    <>
      <div>{state}</div>
      <button onClick={handleClick} />
    </>
  )
}
复制代码

上述代码触发三次 handleClick 后你以为答案会是什么?可能答案与你所想的不大同样,结果是:

0 1 2

由于每次 render 都有一份新的状态,所以上述代码中的 setTimeout 使用产生了一个闭包,捕获了每次 render 后的 state,也就致使了输出了 0、一、2。

若是你但愿输出的内容是最新的 state 的话,能够经过 useRef 来保存 state。前文讲过 ref 在组件中只存在一份,不管什么时候使用它的引用都不会产生变化,所以能够来解决闭包引起的问题。

function App() {
  const [state, setState] = React.useState(0)
  // 用 ref 存一下
  const currentState = React.useRef(state)
  // 每次渲染后更新下值
  useEffect(() => {
    currentState.current = state
  })

  const handleClick = () => {
    setState(state + 1)
    // 这样定时器里经过 ref 拿到最新值
    setTimeout(() => {
      console.log(currentState.current)
    }, 2000)
  }

  return (
    <>
      <div>{state}</div>
      <button onClick={handleClick} />
    </>
  )
}
复制代码

其实闭包引起的问题多半是保存了 old 的值,只要想办法拿到最新的值其实基本上就解决问题了。

写在最后

若是你以为我有遗漏什么或者写的不对的,欢迎指出。

我很想听听你的想法,谢谢阅读。

微信扫码关注公众号,订阅更多精彩内容 加笔者微信群聊技术
相关文章
相关标签/搜索