整理自gitHub笔记

一、误区 :

useCallback是解决函数组件过多内部函数导致的性能问题

使用函数组件时经常定义一些内部函数,总觉得这会影响函数组件性能。也以为useCallback就是解决这个问题的,其实不然(Are Hooks slow because of creating functions in render?):

  1. JS内部函数创建是非常快的,这点性能问题不是个问题;
  2. 得益于相对于 class 更轻量的函数组件,以及避免了 HOC, renderProps 等等额外层级,函数组件性能差不到那里去;
  3. 其实使用useCallback会造成额外的性能;
    因为增加了额外的deps变化判断。
  4. useCallback其实也并不是解决内部函数重新创建的问题。
    仔细看看,其实不管是否使用useCallback,都无法避免重新创建内部函数

    export default function Index() {    const [clickCount, increaseCount] = useState(0);    // 没有使用`useCallback`,每次渲染都会重新创建内部函数    const handleClick = () => {        console.log('handleClick');        increaseCount(clickCount + 1);    }    // 使用`useCallback`,但也每次渲染都会重新创建内部函数作为`useCallback`的实参    const handleClick = useCallback(() => {        console.log('handleClick');        increaseCount(clickCount + 1);    }, [])    return (        <div>            <p>{clickCount}</p>            <Button handleClick={handleClick}>Click</Button>        </div>    )}

二、useCallback解决的问题

useCallback其实是利用memoize减少不必要的子组件重新渲染

import React, { useState, useCallback } from 'react'function Button(props) {    const { handleClick, children } = props;    console.log('Button -> render');    return (        <button onClick={handleClick}>{children}</button>    )}const MemoizedButton = React.memo(Button);export default function Index() {    const [clickCount, increaseCount] = useState(0);        const handleClick = () => {        console.log('handleClick');        increaseCount(clickCount + 1);    }    return (        <div>            <p>{clickCount}</p>            <MemoizedButton handleClick={handleClick}>Click</MemoizedButton>        </div>    )}

即使使用了React.memo修饰了Button组件,但是每次点击【Click】btn都会导致Button组件重新渲染,因为:

  1. Index组件state发生变化,导致组件重新渲染;
  2. 每次渲染导致重新创建内部函数handleClick
  3. 进而导致子组件Button也重新渲染。

使用useCallback优化:

import React, { useState, useCallback } from 'react'function Button(props) {    const { handleClick, children } = props;    console.log('Button -> render');    return (        <button onClick={handleClick}>{children}</button>    )}const MemoizedButton = React.memo(Button);export default function Index() {    const [clickCount, increaseCount] = useState(0);    // 这里使用了`useCallback`    const handleClick = useCallback(() => {        console.log('handleClick');        increaseCount(clickCount + 1);    }, [])    return (        <div>            <p>{clickCount}</p>            <MemoizedButton handleClick={handleClick}>Click</MemoizedButton>        </div>    )}

三、useCallback的问题

3.1 useCallback的实参函数读取的变量是变化的(一般来自state, props)

export default function Index() {    const [text, updateText] = useState('Initial value');    const handleSubmit = useCallback(() => {        console.log(`Text: ${text}`); // BUG:每次输出都是初始值    }, []);    return (        <>            <input value={text} onChange={(e) => updateText(e.target.value)} />            <p onClick={handleSubmit}>useCallback(fn, deps)</p>         </>    )}

修改input值,handleSubmit 处理函数的依旧输出初始值。
如果useCallback的实参函数读取的变量是变化的,记得写在依赖数组里。

export default function Index() {    const [text, updateText] = useState('Initial value');    const handleSubmit = useCallback(() => {        console.log(`Text: ${text}`); // 每次输出都是初始值    }, [text]); // 把`text`写在依赖数组里    return (        <>            <input value={text} onChange={(e) => updateText(e.target.value)} />            <p onClick={handleSubmit}>useCallback(fn, deps)</p>         </>    )}

虽然问题解决了,但是方案不是最好的,因为input输入框变化太频繁,useCallback存在的意义没啥必要了。

3.2 How to read an often-changing value from useCallback?

还是上面例子,如果子组件比较耗时,问题就暴露了:

// 注意:ExpensiveTree 比较耗时记得使用`React.memo`优化下,要不然父组件优化也没用const ExpensiveTree = React.memo(function (props) {    console.log('Render ExpensiveTree')    const { onClick } = props;    const dateBegin = Date.now();    // 很重的组件,不优化会死的那种,真的会死人    while(Date.now() - dateBegin < 600) {}    useEffect(() => {        console.log('Render ExpensiveTree --- DONE')    })    return (        <div onClick={onClick}>            <p>很重的组件,不优化会死的那种</p>        </div>    )});export default function Index() {    const [text, updateText] = useState('Initial value');    const handleSubmit = useCallback(() => {        console.log(`Text: ${text}`);    }, [text]);    return (        <>            <input value={text} onChange={(e) => updateText(e.target.value)} />            <ExpensiveTree onClick={handleSubmit} />        </>    )}

问题:更新input值,发现比较卡顿。

3.2.1 useRef解决方案

优化的思路:

  1. 为了避免子组件ExpensiveTree在无效的重新渲染,必须保证父组件re-render时handleSubmit属性值不变;
  2. handleSubmit属性值不变的情况下,也要保证其能够访问到最新的state。
export default function Index() {    const [text, updateText] = useState('Initial value');    const textRef = useRef(text);    const handleSubmit = useCallback(() => {        console.log(`Text: ${textRef.current}`);    }, [textRef]);    useEffect(() => {        console.log('update text')        textRef.current = text;    }, [text])    return (        <>            <input value={text} onChange={(e) => updateText(e.target.value)} />            <ExpensiveTree onClick={handleSubmit} />        </>    )}

原理:

  1. handleSubmit由原来直接依赖text变成了依赖textRef,因为每次re-render时textRef不变,所以handleSubmit不变;
  2. 每次text更新时都更新textRef.current。这样虽然handleSubmit不变,但是通过textRef也是能够访问最新的值。

useRef+useEffect这种解决方式可以形成一种固定的“模式”:

export default function Index() {    const [text, updateText] = useState('Initial value');    const handleSubmit = useEffectCallback(() => {        console.log(`Text: ${text}`);    }, [text]);    return (        <>            <input value={text} onChange={(e) => updateText(e.target.value)} />            <ExpensiveTree onClick={handleSubmit} />        </>    )}function useEffectCallback(fn, dependencies) {    const ref = useRef(null);    useEffect(() => {        ref.current = fn;    }, [fn, ...dependencies])    return useCallback(() => {        ref.current && ref.current(); // 通过ref.current访问最新的回调函数    }, [ref])}
  1. 通过useRef保持变化的值,
  2. 通过useEffect更新变化的值;
  3. 通过useCallback返回固定的callback。

3.2.2 useReducer解决方案

const ExpensiveTreeDispatch = React.memo(function (props) {    console.log('Render ExpensiveTree')    const { dispatch } = props;    const dateBegin = Date.now();    // 很重的组件,不优化会死的那种,真的会死人    while(Date.now() - dateBegin < 600) {}    useEffect(() => {        console.log('Render ExpensiveTree --- DONE')    })    return (        <div onClick={() => { dispatch({type: 'log' })}}>            <p>很重的组件,不优化会死的那种</p>        </div>    )});function reducer(state, action) {    switch(action.type) {        case 'update':            return action.preload;        case 'log':            console.log(`Text: ${state}`);               return state;         }}export default function Index() {    const [text, dispatch] = useReducer(reducer, 'Initial value');    return (        <>            <input value={text} onChange={(e) => dispatch({                type: 'update',                 preload: e.target.value            })} />            <ExpensiveTreeDispatch dispatch={dispatch} />        </>    )}

原理:

  1. dispatch自带memoize, re-render时不会发生变化;
  2. reducer函数里可以获取最新的state

We recommend to pass dispatch down in context rather than individual callbacks in props.

React官方推荐使用context方式代替通过props传递callback方式。上例改用context传递callback函数:

function reducer(state, action) {    switch(action.type) {        case 'update':            return action.preload;        case 'log':            console.log(`Text: ${state}`);               return state;         }}const TextUpdateDispatch = React.createContext(null);export default function Index() {    const [text, dispatch] = useReducer(reducer, 'Initial value');    return (        <TextUpdateDispatch.Provider value={dispatch}>            <input value={text} onChange={(e) => dispatch({                type: 'update',                 preload: e.target.value            })} />            <ExpensiveTreeDispatchContext dispatch={dispatch} />        </TextUpdateDispatch.Provider>    )}const ExpensiveTreeDispatchContext = React.memo(function (props) {    console.log('Render ExpensiveTree')    // 从`context`获取`dispatch`    const dispatch = useContext(TextUpdateDispatch);    const dateBegin = Date.now();    // 很重的组件,不优化会死的那种,真的会死人    while(Date.now() - dateBegin < 600) {}    useEffect(() => {        console.log('Render ExpensiveTree --- DONE')    })    return (        <div onClick={() => { dispatch({type: 'log' })}}>            <p>很重的组件,不优化会死的那种</p>        </div>    )});