整理自gitHub笔记
一、误区 :
useCallback是解决函数组件过多内部函数导致的性能问题
使用函数组件时经常定义一些内部函数,总觉得这会影响函数组件性能。也以为useCallback就是解决这个问题的,其实不然(Are Hooks slow because of creating functions in render?):
JS内部函数创建是非常快的,这点性能问题不是个问题;
得益于相对于 class 更轻量的函数组件,以及避免了 HOC, renderProps 等等额外层级,函数组件性能差不到那里去;
其实使用useCallback会造成额外的性能;
因为增加了额外的deps变化判断。
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 (
{clickCount}
Click
)
}
二、useCallback解决的问题
useCallback其实是利用memoize减少不必要的子组件重新渲染
import React, { useState, useCallback } from 'react'
function Button(props) {
const { handleClick, children } = props;
console.log('Button -> render');
return (
{children}
)
}
const MemoizedButton = React.memo(Button);
export default function Index() {
const [clickCount, increaseCount] = useState(0);
const handleClick = () => {
console.log('handleClick');
increaseCount(clickCount + 1);
}
return (
{clickCount}
Click
)
}
即使使用了React.memo修饰了Button组件,但是每次点击【Click】btn都会导致Button组件重新渲染,因为:
Index组件state发生变化,导致组件重新渲染;
每次渲染导致重新创建内部函数handleClick,
进而导致子组件Button也重新渲染。
使用useCallback优化:
import React, { useState, useCallback } from 'react'
function Button(props) {
const { handleClick, children } = props;
console.log('Button -> render');
return (
{children}
)
}
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 (
{clickCount}
Click
)
}
三、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 (
<>
updateText(e.target.value)} />
useCallback(fn, deps)
>
)
}
修改input值,handleSubmit处理函数的依旧输出初始值。
如果useCallback的实参函数读取的变量是变化的,记得写在依赖数组里。
export default function Index() {
const [text, updateText] = useState('Initial value');
const handleSubmit = useCallback(() => {
console.log(`Text: ${text}`); // 每次输出都是初始值
}, [text]); // 把`text`写在依赖数组里
return (
<>
updateText(e.target.value)} />
useCallback(fn, deps)
>
)
}
虽然问题解决了,但是方案不是最好的,因为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 (
很重的组件,不优化会死的那种
)
});
export default function Index() {
const [text, updateText] = useState('Initial value');
const handleSubmit = useCallback(() => {
console.log(`Text: ${text}`);
}, [text]);
return (
<>
updateText(e.target.value)} />
>
)
}
问题:更新input值,发现比较卡顿。
3.2.1 useRef解决方案
优化的思路:
为了避免子组件ExpensiveTree在无效的重新渲染,必须保证父组件re-render时handleSubmit属性值不变;
在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 (
<>
updateText(e.target.value)} />
>
)
}
原理:
handleSubmit由原来直接依赖text变成了依赖textRef,因为每次re-render时textRef不变,所以handleSubmit不变;
每次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 (
<>
updateText(e.target.value)} />
>
)
}
function useEffectCallback(fn, dependencies) {
const ref = useRef(null);
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies])
return useCallback(() => {
ref.current && ref.current(); // 通过ref.current访问最新的回调函数
}, [ref])
}
通过useRef保持变化的值,
通过useEffect更新变化的值;
通过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 (
很重的组件,不优化会死的那种
)
});
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 (
<>
dispatch({
type: 'update',
preload: e.target.value
})} />
>
)
}
原理:
dispatch自带memoize, re-render时不会发生变化;
在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 (
dispatch({
type: 'update',
preload: e.target.value
})} />
)
}
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 (
很重的组件,不优化会死的那种
)
});
关于找一找教程网
本站文章仅代表作者观点,不代表本站立场,所有文章非营利性免费分享。
本站提供了软件编程、网站开发技术、服务器运维、人工智能等等IT技术文章,希望广大程序员努力学习,让我们用科技改变世界。
[一直以来`useCallback`的使用姿势都不对]http://www.zyiz.net/tech/detail-147073.html