react Hooks
react hooks是react在16.8版本出现的,它的出现是为了可以只用函数组件就可以写出全功能的组件,实际上可以被认为是函数组件的加强版。
官方介绍:**Hooks是 React 16.8 中的新增功能。它们让您无需编写类即可使用状态和其他 React 功能。
本文所要总结到的memo,useCallback,useMemo即为函数组件中性能优化相关的钩子
memo
React.memo(component, myFunc)接受两个参数,一个是自定义函数,一个是比较函数。该方法类似react class组件的shouldComponentUpdate以及pureComponent, 其中第二个参数用来判断该组件需不需要重新渲染,第二个参数省略的情况下,默认会对传到该组件的props进行浅比较
以下demo在父组件中state变化了之后子组件也会触发相应的更新,但其实子组件没有接收任何props,只是渲染了一个不变的文案,那么此时这个子组件重新渲染就是没有必要的。
const ChildComponent = () => {
console.log('子组件执行了');
return (
<p>我是子组件的内容</p>
)
};
const ParentComponent = () => {
// console.log('父组件执行了');
const [count, setCount] = useState<Number>(1);
const changeCount = () => {
setCount(count + 1);
};
return (
<>
<button onClick={changeCount}>点击</button>
<p>count is: </p>
<p>{count}</p>
<ChildComponent />
</>
)
};
在上一个demo中,我们引入React.memo。来看看控制台的打印情况
const ChildComponent = memo(() => {
console.log('子组件执行了');
return (
<p>我是子组件的内容</p>
)
});
const ParentComponent = () => {
console.log('父组件执行了');
const [count, setCount] = useState<Number>(1);
const changeCount = () => {
setCount(count + 1);
};
return (
<>
<button onClick={changeCount}>点击</button>
<p>count is: </p>
<p>{count}</p>
<ChildComponent />
</>
)
};
此时子组件只会在组件挂载时渲染一次,父组件再怎么更新也不会触发子组件的重新渲染了。
另外,刚刚我们上面说到了memo还接收第二个参数,为自定义的比较函数,当我们传入了这个自定义函数,子组件是否重新渲染则取决于这个函数的返回值,该函数会比较新旧props是否一致,如果一致则返回true,此时子组件不会重新渲染,反之返回false,子组件会重新渲染。
const myEquareFunc = (prevProps, nextProps) => {
console.log('prevProps is:', prevProps);
console.log('nextProps is: ', nextProps);
// 比较新旧props是否相等,相等返回true,否则返回false
};
useCallBack
当我们给子组件传入props后,此时用memo可能不会起作用,子组件还是会执行。 原因在于匿名函数在每次渲染后的引用都不同,从而导致子组件的重新渲染。
const ChildComponent = memo(({ text, changeText }) => {
console.log('子组件执行了');
return (
<>
<p>text is: {text}</p>
<button onClick={()=>changeText('改变文案')}>按钮</button>
</>
)
});
const ParentComponent = () => {
const [number, setNumber] = useState<number>(1);
const [text, setText] = useState<string>('我是父组件传入子组件的文案');
const handleChange = () => {
setNumber(number + 1);
};
const changeText = (newText) => {
setText(newText);
};
return (
<>
<button onClick={handleChange}>clike me</button>
<p>count: {number}</p>
<ChildComponent text={text} changeText={changeText} />
</>
)
};
从以上demo可以看出,虽然子组件使用了memo,但是在父组件setNumber的每次还是会重新渲染子组件,这个时候就可以用到useCallback来优化这种行为了。
const ChildComponent = memo(({ text, changeText }) => {
console.log('子组件执行了');
return (
<>
<p>text is: {text}</p>
<button onClick={()=>changeText('改变文案')}>按钮</button>
</>
)
});
const ParentComponent = () => {
const [number, setNumber] = useState<number>(1);
const [text, setText] = useState<string>('我是父组件传入子组件的文案');
const handleChange = () => {
setNumber(number + 1);
};
const changeText = useCallback((newText) => {
setText(newText);
}, []); // 此依赖项必不可少,否则会每次渲染都会执行,从而useCallback就没有意义了
return (
<>
<button onClick={handleChange}>clike me</button>
<p>count: {number}</p>
<ChildComponent text={text} changeText={changeText} />
</>
)
};
此时就可以看到,子组件只会在初始化及点击修改文案的时候渲染一次,其他时候父组件的state再怎么更新,都不会触发子组件的重新渲染了。
useMemo
useMemo也接收两个参数,官方描述:传递一个“create”函数和一个依赖数组。useMemo
仅当依赖项之一发生更改时才会重新计算记忆值。这种优化有助于避免在每次渲染时进行昂贵的计算。
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
。
直接上demo
const ChildComponent = memo(({ infos, changeText }) => {
console.log('子组件执行了');
return (
<>
<p>text is: {infos.text}</p>
<button onClick={()=>changeText('改变文案')}>按钮</button>
</>
);
});
const ParentComponent = () => {
const [number, setNumber] = useState<number>(1);
const [text, setText] = useState<string>('我是父组件传入子组件的文案');
const handleChange = () => {
setNumber(number + 1);
};
const changeText = useCallback((newText) => {
setText(newText);
}, []); // 此依赖项必不可少,否则会每次渲染都会执行,从而useCallback就没有意义了
return (
<>
<button onClick={handleChange}>clike me</button>
<p>count: {number}</p>
<ChildComponent infos={{text}} changeText={changeText} />
</>
)
};
由以上代码可知,父组件中的state更新后,子组件还是会渲染。这是因为每次都会生成一个包含text的新对象,监听到props发生变化,自然就会重新渲染子组件。
const ChildComponent = memo(({ infos, changeText }) => {
console.log('子组件执行了');
return (
<>
<p>text is: {infos.text}</p>
<button onClick={()=>changeText('改变文案')}>按钮</button>
</>
);
});
const ParentComponent = () => {
const [number, setNumber] = useState<number>(1);
const [text, setText] = useState<string>('我是父组件传入子组件的文案');
const handleChange = () => {
setNumber(number + 1);
};
const changeText = useCallback((newText) => {
setText(newText);
}, []); // 此依赖项必不可少,否则会每次渲染都会执行,从而useCallback就没有意义了
const result = useMemo(()=>({
text,
}), [text]);
return (
<>
<button onClick={handleChange}>clike me</button>
<p>count: {number}</p>
<ChildComponent infos={result} changeText={changeText} />
</>
)
};
export default ParentComponent;
当我们使用useMemo包裹所传入的对象时,则发现子组件不会再重新渲染了,这样就很好的做到了不必要的渲染。
在react官方文档中,useMemo还可以用于减少计算的量,缓存运算量比较大的函数:
const [count, setCount] = useState<number>(0);
const expensiveFn = () => {
console.log('方法执行了');
let result = 0;
for(let i = 0; i < 10000; i++) {
result += i;
}
return result;
};
const base = expensiveFn();
return (
<>
<h1>count: {count} </h1>
<button onClick={()=>setCount(count + base)}> click me </button>
</>
);
这个demo每次点击按钮,组件都会重新渲染一次,方法也会重新渲染一次。但是实际上expensiceFn中是一个花费比较高的函数,且函数返回值是恒定的,这样就造成了不必要的性能浪费。 这时候就可以用useMemo将计算后的值缓存起来。
const MyComponent = () => {
const [count, setCount] = useState<number>(0);
const expensiveFn = () => {
console.log('方法执行了');
let result = 0;
for(let i = 0; i < 10000; i++) {
result += i;
}
return result;
};
const base = useMemo(expensiveFn, []);
return (
<>
<h1>count: {count} </h1>
<button onClick={()=>setCount(count + base)}> click me </button>
</>
)
};
这样的话expensiveFn的值就只会在初始的时候计算一次。
useMemo的第一个参数是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为useMemo的返回值,第二个参数是一个数组依赖,如果数组里的值有变化,那么就会重新执行第一个参数里面的函数,并将函数返回的值缓存起来作为useMemo的返回值。
如果没有提供依赖数组,useMemo在每次渲染时都会计算新的值。