useCallback
基本示例
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个memoized
的函数,内联回调函数及依赖项数组作为参数传入useCallback
,该回调函数只有在依赖项改变的时候才会更新,避免非必要的渲染。我在实际工作中因为用了eslint
的一个配置,依赖项自动给加上。
react-hooks/exhaustive-deps: 'error'/'warn'
然后也不具体看依赖项,几次导致循环调用(后面我给出发生循坏调用的代码),所以后面我们就去掉了这个eslint-rule
Memoization
Memoization这里很有必要提下这个。理解为缓存,看了下hooks的源码,基本上都用了Memoization
这个概念。
简易版的useCallback
let hookStates = [];
let hookIndex = 0;
function useCallbacks(callback, dependencies) {
if (hookStates[hookIndex]) { // 说明不是第一次渲染
let [lastCallback, lastDependencies] = hookStates[hookIndex];
let same = dependencies.every((item, index) => item === lastDependencies);
if (same) {
hookIndex++;
return lastCallback;
}
}
// 第一次渲染 或者 不是第一次但是依赖项相同,都返回新的
hookStates[hookIndex++] = [callback, dependencies];
return callback;
}
源码
export function useCallback<T>(
callback: T,
inputs: Array<mixed> | void | null,
): T {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
// 以上两句几乎在hooks中都用到了
const nextInputs =
inputs !== undefined && inputs !== null ? inputs : [callback]; // 新的依赖项,如果为undefined或者null的话, 则用callback,否则用依赖项
const prevState = workInProgressHook.memoizedState;
if (prevState !== null) {
const prevInputs = prevState[1]; // 依赖项
if (areHookInputsEqual(nextInputs, prevInputs)) { //对比依赖项,相同从memoizedState获取
return prevState[0]; // 返回上一个memoizedState的callback
}
}
workInProgressHook.memoizedState = [callback, nextInputs]; // 首次或非首次不相同存入memoizedState
return callback; // 返回callback
}
useMemo
基本用法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
两个参数。一个回调,一个依赖项,与useCallback没什么不同,返回一个memoized
的函数。甚至示例也给出了一个隐藏的提示,昂贵的计算时才用。
简易版的useMemo
function useMemos(nextCreate, dependencies) {
if (hookStates[hookIndex]) { // 说明不是第一次渲染
let [lastMemo, lastDependencies] = hookStates[hookIndex];
let same = dependencies.every((item, index) => item === lastDependencies);
if (same) {
hookIndex++;
return lastMemo;
}
}
const nextValue = nextCreate(); // 此处有点不用
// 第一次渲染 或者 不是第一次但是依赖项相同,都返回新的
hookStates[hookIndex++] = [nextValue, dependencies];
return nextValue;
}
源码
export function useMemo<T>(
nextCreate: () => T,
inputs: Array<mixed> | void | null,
): T {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
const nextInputs =
inputs !== undefined && inputs !== null ? inputs : [nextCreate];
const prevState = workInProgressHook.memoizedState;
if (prevState !== null) {
const prevInputs = prevState[1];
if (areHookInputsEqual(nextInputs, prevInputs)) {
return prevState[0];
}
}
const nextValue = nextCreate(); // 除了这句,其他的和useCallback一模一样,不能你从否从这句看出useMemo与useCallback分别适用于什么场景
workInProgressHook.memoizedState = [nextValue, nextInputs];
return nextValue;
}
使用场景
源码的区别
有趣的是在源码中,两个hooks在第一个入参的时候声明的类型有所区别,不知道你没有注意的
useCallback
export function useCallback<T>(
callback: T,
inputs: Array<mixed> | void | null,
): T {
}
useMemo
export function useMemo<T>(
nextCreate: () => T,
inputs: Array<mixed> | void | null,
): T {
}
感觉useMemo
用来专门处理函数,虽然useCallback
也可以做到,但我看了不少例子。useMemo这样子用,这好像形成了一个约定
useMemo
const initialCandies = React.useMemo(
() => ['snickers', 'skittles', 'twix', 'milky way'],
[],
)
useCallback
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
const dispenseCallback = React.useCallback(dispense, [])
究竟何时用
推荐阅读usememo-and-usecallback,这篇是译文。原作者是Kent C. Dodds
引用相等
理解为依赖项引用(通常表现为非原始类型,如对象、数组、函数。 他们看来类型和属性都是一样的,但是引用却不同)相等的情况下,可以用useCallback 或者useMemo,如
function Foo({bar, baz}) {
const options = {bar, baz}
React.useEffect(() => {
buzz(options)
}, [options]) // we want this to re-run if bar or baz change
return <div>foobar</div>
}
function Blub() {
return <Foo bar="bar value" baz={3} />
}
能看出这段代码有问题吗。 每次都会调用useEffect
的回调。为了更好的测试,我将代码稍微改变了下
class App extends React.Component {
constructor() {
super();
this.state = {
bar: ['1'],
baz: ['1'],
};
}
handleChangeBazInput = (e) => {
this.setState({
baz: [e.target.value],
});
};
handleChangeBarInput = (e) => {
this.setState({
bar: [e.target.value],
});
};
render() {
return (
<div>
<input type="text" onBlur={this.handleChangeBazInput}></input>
<input type="text" onBlur={this.handleChangeBarInput}></input>
<Foo baz={this.state.baz} bar={this.state.bar}></Foo>
</div>
);
}
}
function Foo({ bar, baz }) {
const options = { bar, baz };
React.useEffect(() => {
console.log(options); // 不管 bar, baz有没有发生变化,这里都会打印出来
}, [options]); // 即使改成改成[baz,bar]也没有用,因为bar、baz是数组
return (
<div>
<div> {baz}</div>
<div> {bar}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
但是如果bar、baz是一般类型的值,如string,就不会啦
this.state = {
bar: '1',
baz: '1',
};
}
handleChangeBazInput = (e) => {
this.setState({
baz: e.target.value,
});
};
handleChangeBarInput = (e) => {
this.setState({
bar: e.target.value,
});
};
React.useEffect(() => {
console.log(options);
}, [bar,baz]); // 注意这里不是options
这是作者给出的实例,我觉得没有很具体
before
function Foo({bar, baz}) {
React.useEffect(() => {
const options = {bar, baz}
buzz(options)
}, [bar, baz]) // we want this to re-run if bar or baz change
return <div>foobar</div>
}
function Blub() {
const bar = () => {}
const baz = [1, 2, 3]
return <Foo bar={bar} baz={baz} />
}
after
function Foo({bar, baz}) {
React.useEffect(() => {
const options = {bar, baz}
buzz(options)
}, [bar, baz])
return <div>foobar</div>
}
function Blub() {
const bar = React.useCallback(() => {}, [])
const baz = React.useMemo(() => [1, 2, 3], [])
return <Foo bar={bar} baz={baz} />
}
我自己尝试用usecallback包了一层,发现和之前以前。当bar、baz是数组时,永远都会打印
function App() {
const [bar, setBar] = useState(["1"]);
const [baz, setBaz] = useState(["1"]);
const handleChangeBazInput = useCallback((e) => {
setBaz([e.target.value]);
}, []);
const handleChangeBarInput = useCallback((e) => {
setBar([e.target.value]);
}, []);
return (
<div>
<input type="text" onBlur={(e) => handleChangeBazInput(e)}></input>
<input type="text" onBlur={(e) => handleChangeBarInput(e)}></input>
<Foo baz={baz} bar={bar}></Foo>
</div>
);
}
function Foo({ bar, baz }) {
const options = { bar, baz };
React.useEffect(() => {
console.log(options); // 这样还是会打印
}, [bar, baz]);
return (
<div>
{" "}
<div>{baz}</div> <div> {bar}</div>
</div>
);
}
不知道我理解是不是有问题,如果你知道的话可以告诉我
巨大开销
这个没啥说的,因为我们工作中很少会碰到巨大的计算,压根用不着
before
function RenderPrimes({iterations, multiplier}) {
const primes = calculatePrimes(iterations, multiplier)
return <div>Primes! {primes}</div>
}
after
function RenderPrimes({iterations, multiplier}) {
const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
iterations,
multiplier,
])
return <div>Primes! {primes}</div>
}
产生死循环的代码
const searchList = useCallback(() => {
fetchMyVisitList({
page: {
pageNo: 1,
pageSize: 20
},
params: {}
}).then(res => {
if (res.success) {
// do something
}
})
}, [list])
useEffect(() => {
searchList()
}, [searchList])
// 所有的依赖是自动加上的
总结
首先说了useCallback
与useMemo
的基本用法,然后写了个简易版的,再从源码分析。思路都是一样的,都用了Memoization
这个概念。接着说了使用的场景,我问了一些人,他们平时很少用这两个hooks,诚如Kent C. Dodds
所言,性能优化都需要成本,当你不需要性能优化时,你根本不需要用,用的话反而效果更不好。最后我写了个关于用useCallback
和不用useCallback
的例子,发现根本没区别,不知道是不是哪里没有理解到位,如果你知道的话可以告诉我