React 如何避免不必要的渲染?
React 中的每一次更新都是从根节点开始的(与Vue的区别,确实比Vue的性能差一点)。由于每次从根节点开始对比DOM_DIFF,所以渲染的工作量很大。因此要尽量减少渲染(自己进行性能优化的原因)。 解决方案: 1、在类组件中:可以使用 React.PureComponent,该API是React的基类,它的核心特性是只有当它的属性or状态发生浅层变化时,才会重新渲染。浅层比较,会检测对象的顶层属性,而不是深度检查,这样可以提高性能。 2、函数组件中:React.memo是一个高阶组件,可用于优化那些仅仅依赖于其属性变化的组件重新渲染的行为。若组件在多次渲染时,没有变化,那么memo就能避免不必要的组件渲染,复用上一次的结果。
- 为了尽量减少组件渲染的次数,需要尽量保持属性不变。
ReactHooks
解决class组件的一些问题:
- this指向不明
- 业务逻辑分散在不同生命周期方法中
- 复用逻辑不方便比较复杂 函数组件虽简单,但它没有状态。
- 为了让函数组件有状态,就出现了ReactHooks 注意:
- 不要在循环、条件or嵌套函数中调用useEffect,此规则针对所有的hook都适用。
- 清除副作用 useEffect会返回一个函数,该函数会在组件卸载前or重新执行新的副作用之前被调用,可在此时清除副作用,如 取消事件监听、取消网络请求、清除定时器。
setState (同步、异步)
- 彻底搞懂setState到底是同步还是异步(一)
- 彻底搞懂setState到底是同步还是异步(二)
- 彻底搞懂setState到底是同步还是异步(三)
- React17与React18是不一样的:在React18之前,在React能管理or控制的范围,如 事件回调、生命周期函数中,都是批量的;但在React管理不到的地方,如 setInterval、setTimeout都是同步的,非批量的;因为在React18之前,是根据一个变量控制的,isBatchingUpdate;在React18之后,不管在什么地方,都是批量的,都是异步的。
- setState优化,当在调用setState时,若传入的新状态 === 旧状态,则没有任何效果,不进行更新。
useReducer
- 当状态变化比较复杂时,可使用reducer。
- useReducer接受一个reducer函数+一个初始状态作为参数,返回当前state和一个与该reducer函数关联的dispatch方法。
const [state,dispatch] = useReducer(reducer, initialState);
useMemo (性能优化)
返回一个记忆后的值,只有当依赖项发生变化时,才会重新计算这个值。保持引用地址不变(堆栈)。
// 使用React.useMemo可缓存对象,该对象可在APP组件 多次渲染时,保持引用地址不变
const displayData = React.useMemo(()=>({count}), [count]);
useCallback (性能优化)
返回一个记忆后的callback函数,返回一个不变的函数,直到依赖项发生变化。保持引用地址不变(堆栈)。
// 使用React.useCallback可缓存回调函数,该函数可在APP组件 多次渲染时,保持引用地址不变
// 入参二:是依赖值的数组,当依赖数组中的值不变时,则复用上一次的值;若变化了,则重新计算新的对象。
const incrementCount = React.useCallback(()=>{
setCount(prevState => prevState + 1);
}, [count]);
useContext
允许无需明确传递props,就能让组件订阅context的变化。
useEffect(重点,宏任务队列)
- 允许在函数组件中,执行有副作用的操作,如 开启定时器、操作dom。
- 与class组件中的生命周期方法,如 componentDidMount、componentDidUpdate、componentWillUnmount类似。
const [count, setCount] = React.useState(0);
React.useEffect(()=>{
//执行副作用操作的函数
//副作用的函数体,当前作用域的代码,默认在每次渲染之后执行
const timer = setInterval(()=>{
setCount(count => count + 1);
}, 1000);
//effect函数返回一个清理函数,此函数在下一次执行effect函数之前被调用
return ()=>{
console.log(`销毁旧定时器`);
clearInterval(timer);
}
},['依赖']) //依赖数组 可选项,若填入,副作用函数仅在这些依赖发生变化时执行;若未填入,则每次渲染都会执行。
// 入参二:当为空数组时,仅渲染一次。
- 副作用函数不是立即执行的,而是包装成一个宏任务(若上一次执行effect函数返回一个清理函数,则在下次执行effect之前,执行上一次返回的清理函数),在浏览器绘制渲染页面之后执行。
- useEffect执行时机比较晚,会在浏览器绘制之后执行。
setTimeout(()=>{
previousCleanup?.(); // 上一次返回的清理函数
const cleanup = effect();
hookStates[hookIndex] = { cleanup, prevDeps: deps };
})
注意:
- 不要在循环、条件or嵌套函数中调用useEffect,此规则针对所有的hook都适用。
- 清除副作用 useEffect会返回一个函数,该函数会在组件卸载前or重新执行新的副作用之前被调用,可在此时清除副作用,如 取消事件监听、取消网络请求、清除定时器。
useLayoutEffect(微任务队列)
- 作用:当需要在React更新DOM之后,但在浏览器绘制之前,读取or修改DOM,这时应该使用
useLayoutEffect,通常用于读取 如 布局、尺寸等与视觉相关的属性;若不关心绘制,只是需要在某些事情发生后执行一些代码,那么使用useEffect。 - 与useEffect具有相同的签名,但也有一些差异。
- 执行时机不同:useEffect在浏览器绘制后
异步执行的,会导致延迟;useLayoutEffect在浏览器执行绘制前同步执行的(可能会阻止浏览器渲染),因此不会延迟。 - 在JavaScript中,实现微任务
Promise.resolve().then()或者queueMicrotask()。 - 事件循环机制,如下图:
useRef
会返回一个可变的ref对象,该对象的current属性可被修改,而修改current属性不会导致组件重新渲染。
useImperativeHandle
- 是一个高级hook,通常与forwardRef搭配使用,允许在使用ref时,自定义暴露给父组件的实例值,而不是默认实例。
function FancyInput(props, forwardRef) {
const inputRef = React.useRef();
//参数1:forward转发过来的ref;参数2:工厂函数,能返回一个对象,此对象传递给ref
React.useImperativeHandle(forwardRef, ()=>({
focus(){
inputRef.current.focus();
}
}));
return <input ref={inputRef} />
}
const ForwardFancyInput = React.forwardRef(FancyInput);
function ParentComponent() {
const inputRef = React.useRef();
const focus = ()=> {
//inputRef.current能获取子组件的真实DOM,缺点:可能不安全,破坏了子组件的封装性
inputRef.current?.focus();
inputRef.current?.remove();
}
return (<>
<ForwardFancyInput ref={inputRef} />
<button onClick={focus}>focus</button>
</>)
}