一直以来`useCallback`的使用姿式都不对

整理自gitHub笔记html

1、误区 :

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

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

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

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

2、useCallback解决的问题

useCallback实际上是利用memoize减小没必要要的子组件从新渲染github

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优化:ide

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

3、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值,发现比较卡顿。spa

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>
    )
});
相关文章
相关标签/搜索