一直以來`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>
    )
});
相關文章
相關標籤/搜索