React Summary (一)

280 阅读6分钟

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 (同步、异步)

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()
  • 事件循环机制,如下图: useLayoutEffect.jpg

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>
    </>)
}