尼采般地抒情

尼采般地抒情

尼采般地抒情

音乐盒

站点信息

文章总数目: 321
已运行时间: 1782

官方文档:React 官方中文文档

📌变量相关

useState

普通定义的变量每次组件渲染都是所定义的值,如果我们需要在每次re-render都记住一个状态的最新值,那就需要使用useState hook

import { useState } from 'React'
const A = () => {
  const [a, setA] = useState('a')
  return <div>a value: {a}</div>
}

和类组件的差异

  1. 改变值的时候不会merge,而是覆盖
  2. 初始值可以是回调函数,但不能是函数否则每次改变都会重新执行一遍该函数,影响性能
import { Button } from 'antd';

export default () => {
  const [count, setCount] = useState(
    [1, 2, 3].reduce((curr, prev) => {
      // eslint-disable-next-line no-param-reassign
      curr += prev;
      console.info('>>> init run >>>');
      return curr;
    }, 0),
  );
  const addCount = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <div className="p-14">
      <Button onClick={addCount}>count: {count}</Button>
    </div>
  );
};

  1. 同样存在批处理

【useState返回的set函数并不会立刻更新状态值,而是会批量更新,所以在set函数执行后,状态可能还是原始值,需要等到下次render值才会更新,addA结果还是+1】

import { Button } from 'antd';

export default () => {
  const [count, setCount] = useState(0);
  const addCount = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };
  return (
    <div className="p-14">
      <Button onClick={addCount}>count: {count}</Button>
    </div>
  );
};

  1. 传入的是值,渲染前会存在覆盖情况,如果传入的是函数,则会保留上一次的状态值计算
import { Button } from 'antd';

export default () => {
  const [count, setCount] = useState(0);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };
  return (
    <div className="p-14">
      <Button onClick={addCount}>count: {count}</Button>
    </div>
  );
};

useRef和forwardRef

文档:脱围机制 – React 中文文档

模拟生命周期/缓存变量

利用useRef创建的普通变量,可以具备“记忆”功能,类似于类的实例属性。利用这一点可以设置一个flag来实现组件仅更新时触发的代码执行时机(所定义的变量与UI无关)

import { Button } from 'antd';

export default () => {
  const [count, setCount] = useState(0);
  const flag = useRef(false);
  useEffect(() => {
    if (flag.current) {
      console.info('>>> only update >>>');
    }
  }, [count]);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
    flag.current = true;
  };
  return (
    <div className="p-14 text-right">
      <Button onClick={addCount}>count: {count}</Button>
    </div>
  );
};

作用在DOM上

且为useRef函数,则为带有键为current值为DOM本身的对象

也可以作用在DOM上的一个函数

import { Button } from 'antd';

export default () => {
  const btn = useRef(null);
  return (
    <div className="p-14 text-right">
      <Button
        onClick={() => {
          console.log(btn);
        }}
      >
        <button ref={btn}>+++</button>
      </Button>
    </div>
  );
};

作用在组件上

useRef不可以作用在组件上,但是子组件有React.forwardRef(组件转发)可以

import { useRef } from 'React'
const Son = React.forwardRef((props, ref) => {
  return <div><input type="text" ref={ref} /></div>
})
const A = () => {
  const obj = useRef()
  // obj => { current: inputDOM }
  return <Son ref={obj} />
}

useImperativeHandle

传递多个ref可以使用useImperativeHandle

父组件:

子组件:

💥副作用相关

useEffect

渲染完成再执行

  1. 模拟各个生命周期的执行时机
import { Button } from 'antd';

export default () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('>>> mount or update >>>');
    return () => {
      console.log('>>> unmount or before update >>>');
    };
  }, [count]);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };
  return (
    <div className="p-14 text-right">
      <Button onClick={addCount}>count: {count}</Button>
    </div>
  );
};

  1. 关于第二个数组参数
    1. 如果不设置,那么在update时期都会执行
    2. 如果设置空数组(且回调函数中没有响应式变量),则只在mount时期执行一次
    3. 如果useEffect回调函数中存在响应式变量,那么第二个参数数组中应当有该响应式变量

useLayoutEffect

  • useEffect:渲染并绘制到屏幕之后执行,异步
  • useLayoutEffect:渲染之后,但绘制到屏幕之前执行,同步
  • 一般如果回调函数中有DOM的相关操作并且会改变样式,用后者,避免DOM渲染闪屏/白屏,但前者性能方面更好

渲染优化

首先明确react组件重渲染的触发条件:

  1. props或者state发生改变触发组件重渲染
  2. 父组件的重渲染触发子组件的重渲染

参考:

1 [非渲染层面] 组件结构设计

分离组件和状态

  • 拆分组件:将大组件拆分成多个小组件,每个小组件只关注自己需要的状态。这可以减少状态变化时影响的范围,避免整个大组件的重新渲染。
  • 提升状态:如果多个组件共享同一份状态,可以考虑将状态提升到它们的共同祖先组件,然后通过 props 传递状态。

使用条件渲染

{msg && (<Child msg={msg} onClick={onChildClick} />)}

使用 Fragment 减少不必要的 DOM 节点

lazy(API)Suspense(API)

当模块化引入(import)组件,但在实际代码中未使用到,代码内部逻辑仍会执行,这个时候可以利用lazy使组件异步化加载,从而达到性能优化效果

import { lazy } from 'react';

const AuthRoute = lazy(() => import('./AuthRoute'));
const ErrorPage = lazy(() => import('@/components/ErrorBoundary'));
const NotFound = lazy(() => import('@/pages/404'));

Suspense则作为中间过渡态

开发者工具分析

Profiler标签

  • 使用 React 开发者工具的 “Profiler” 标签来分析组件的渲染次数和性能瓶颈,进而优化代码。
const onRenderCallback = (
  id: string,
  phase: string,
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number,
) => {
  console.log({ id, phase, actualDuration, baseDuration, startTime, commitTime });
};
<Profiler id="App" onRender={onRenderCallback}>
  <div style={{
  marginTop: '200px',
  textAlign: 'right'
}}>
    <button onClick={onAppClick}>
      count is {count}
    </button>
    {msg && (
  <Suspense fallback={<div>Loading...</div>}>
    <Child msg={msg} onClick={onChildClick} />
  </Suspense>
)}
  </div>
</Profiler>

2 [render阶段] 跳过不必要的组件更新

memo优化子组件

未使用memo的渲染, 当父组件重新渲染,即使子组件未发生变化, 子组件也会重新渲染

import { Button } from 'antd';

const Child = () => {
  console.log('>>> child component render >>>');
  return <div>child component</div>;
};

export default () => {
  const [count, setCount] = useState(0);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };
  return (
    <div className="p-14 text-right">
      <Button onClick={addCount}>count: {count}</Button>
      <Child />
    </div>
  );
};

memo类似类组件中的PureComponent性能优化组件

  1. 对传入的 props 进行浅比较,如果 props 没有变化,则不会重新渲染组件
  2. 函数组件中当响应变量的值没有发生改变,不会重新渲染,和类组件不一样
  3. 当组件的值发生改变才进行render,反之不进行render
import { Button } from 'antd';

const Child = memo((props: any) => {
  console.log('>>> child component render >>>');
  return <div>child component {props.child}</div>;
});

export default () => {
  const [count, setCount] = useState(0);
  const [child, setChild] = useState(0);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };
  return (
    <div className="p-14 text-right">
      <Button onClick={addCount}>not change: {count}</Button>
      <Button
        onClick={() => {
          setChild(child => child + 1);
        }}
      >
        change: {child}
      </Button>
      <Child child={child} />
    </div>
  );
};

避免匿名函数和对象的重新创建

import React, { useState } from 'react'

const Child = React.memo((props?: {
  msg?: string
  onClick?: () => void
}) => {
  console.info('[child render]')
  return (
    <div>
      {props?.msg || 'child default msg value'}
    </div>
  )
})

function App() {
  console.info('[App render]')
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('hello');

  return (
      <div style={{
        marginTop: '200px',
        textAlign: 'right'
      }}>
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <Child msg={msg} onClick={() => console.log('test lambda function rerender too.')} />
      </div>
  )
}

export default App

比如这种情况即便Child组件用了memo,但是每次App重新渲染,Child也会跟着重新渲染。因为匿名函数的引用发生了变化

[App render]
[child render]

为什么要避免匿名函数和对象?

每次组件重新渲染时,JavaScript 会重新创建这些匿名函数和对象。这些新创建的函数和对象在内存中是不同的,即使它们的内容相同。这会导致以下问题:

  1. 导致子组件不必要的重新渲染: 当子组件接收到新的匿名函数或对象作为 props 时,即使这些 props 的内容没有变化,React 仍然认为这些 props 发生了变化,因为它们是新的引用,进而导致子组件重新渲染。
  2. 性能问题: 频繁创建新的函数和对象会增加内存使用和垃圾回收的负担,尤其是在组件频繁更新的情况下,这会对性能产生负面影响。

useMemo和useCallback

React函数式组件在重新渲染时候,代码执行会将函数体重新执行,即便结果不是重新更新DOM。如果函数体的代码逻辑复杂会带来不小的性能损耗,如果能够对这过程中的部分“量”进行“记忆”,则会较大提升性能。

响应式变量内部会自动进行记忆,但是如果是非响应式变量,比如一个对象或是一个函数传入组件,这其实也是改变了的,原因是对象的引用改变,依然会引起DOM的重新渲染,而利用useMemo或是useCallback创建的对象(对象、数组、函数)则会保留记忆功能。

useCallback

useCallback 缓存函数

未使用前

import { Button } from 'antd';

const Child = memo((props: any) => {
  console.log('>>> child component render >>>');
  return <div>child component {props.child}</div>;
});

export default () => {
  const [count, setCount] = useState(0);
  const [child, setChild] = useState(0);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };
  const changeChild = () => {
    setChild(child => child + 1);
  };
  return (
    <div className="p-14 text-right">
      <Button onClick={addCount}>parent count: {count}</Button>
      <Child child={child} click={changeChild} />
    </div>
  );
};

使用后

import { Button } from 'antd';

const Child = memo((props: any) => {
  console.log('>>> child component render >>>');
  return <div>child component {props.child}</div>;
});

export default () => {
  const [count, setCount] = useState(0);
  const [child, setChild] = useState(0);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };
  const changeChild = useCallback(() => {
    setChild(child => child + 1);
  }, []);
  return (
    <div className="p-14 text-right">
      <Button onClick={addCount}>parent count: {count}</Button>
      <Child child={child} click={changeChild} />
    </div>
  );
};

useMemo

useMemo 缓存(复杂)值

传入一个必须带有返回值的函数

import { Button } from 'antd';

const Child = memo((props: any) => {
  const sum = useMemo(
    () =>
      [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((acc, cur) => {
        // eslint-disable-next-line no-param-reassign
        acc += cur;
        console.info('>>> calculate sum >>>');
        return acc;
      }, 0),
    [],
  );
  console.log('>>> child component render >>>', sum);
  return <div>child component {props.child}</div>;
});

export default () => {
  const [count, setCount] = useState(0);
  const [child, setChild] = useState(0);
  const addCount = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };
  return (
    <div className="p-14 text-right">
      <Button onClick={addCount}>not change: {count}</Button>
      <Button
        onClick={() => {
          setChild(child => child + 1);
        }}
      >
        change: {child}
      </Button>
      <Child child={child} />
    </div>
  );
};

如果不使用useMemo,那么每次child渲染都会执行一遍sum的计算函数,也就是多了十次calculate sum打印

  1. 两者的第二个参数都是依赖数组,同useEffect
import { useCallback, useMemo } from 'React'
const A = () => {
  // 1. every render -> run
  const fun = () => {}
  // 2. useCallback (ignore const error...)
  const fun = useCallback(() => {}, [])
  // 3. useMemo (ignore const error...)
  const fun = useMemo(() => () => {}, [])
  const a = useMemo(() => [1, 2, 3], [])
  
  return <div onClick={fun} a={a}></div>
}

3 [commit阶段] 减少提交耗时

批量状态更新

批量更新是指 React 将多次 state 更新进行合并处理,最终只进行一次渲染,以获得更好的性能。

例如,如果在同一个点击事件中有两个状态更新,React 总是会把它们批量处理成一个重新渲染。

从 React 18 的 createRoot 开始,不论在哪里, 所有更新都将自动进行批量更新。

这意味着 setTimeout、promises、原生事件处理函数或其他任何事件的批量更新都将与 React 事件一样,以相同的方式进行批量更新。

比如下面这个组件,在React 18之前会渲染两次,但是React 18可以自动批处理

function App() {
  console.info('[App render]')
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('hello');
  const onChildClick = useCallback(() => {
    console.log('test lambda function rerender too.')
  }, [])
  const onAppClick = () => {
    fetch(`https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=main`)
    .then(res => res.json())
    .then(data => {
      console.info('>>> fetch data >>>', data)
      setCount((count) => count + 1)
      setMsg('hello world')
    })
  }
  return (
      <div style={{
        marginTop: '200px',
        textAlign: 'right'
      }}>
        <button onClick={onAppClick}>
          count is {count}
        </button>
        {msg && (<Child msg={msg} onClick={onChildClick} />)}
      </div>
  )
}

如果想取消批处理

function App() {
  console.info('[App render]')
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('hello');
  const onChildClick = useCallback(() => {
    console.log('test lambda function rerender too.')
  }, [])
  const onAppClick = () => {
    fetch(`https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=main`)
    .then(res => res.json())
    .then(data => {
      console.info('>>> fetch data >>>', data)
      flushSync(() => {
        setCount((count) => count + 1)
      });
      flushSync(() => {
        setMsg('hello world')
      });
    })
  }
  return (
      <div style={{
        marginTop: '200px',
        textAlign: 'right'
      }}>
        <button onClick={onAppClick}>
          count is {count}
        </button>
        {msg && (<Child msg={msg} onClick={onChildClick} />)}
      </div>
  )
}

可以看到多执行了一次[App render]

https://juejin.cn/post/6982433531792195621

避免不必要的 Context 重新渲染

  • Context 是非常强大的工具,但不慎使用可能导致不必要的重新渲染。将 Context 提供者尽量拆分成多个小的上下文,或者在必要时使用 useMemo 缓存 Context 值。
const value = useMemo(() => ({ state, dispatch }), [state, dispatch]);

return (
  <MyContext.Provider value={value}>
    {children}
  </MyContext.Provider>
);


🎈其他Hooks

useContext和createContext(API)

跨组件通信

import { useContext, createContext } from 'React'

// not Provider value -> render default value
const C = createContext('default value...')

const GSon = () => {
  const value = useContext(C)
  // value -> something...
  return <div>GSon template...</div>
}
const Son = () => {
  return <GSon />
}

const A = () => {

  return (
    <C.Provider value='something...'>
    	<Son ref={obj} />
    </C.Provider>
  )
}

useReducer

管理多个有关联的响应式变量

import { useReducer } from 'React

const loginState = { isLogin: true, isLogout: false }
const loginReducer = (state, action) => {
  switch(action.type) {
    case 'login':
      return { isLogin: true, isLogout: false }
    case 'logout':
      return { isLogin: false, isLogout: true }
    default:
      return new Error()
  }
}

const A = () => {
  const [state, LoginDispatch] = useReducer(loginReducer, loginState)
  const clickEvent = () => {
    loginDispatch({ type: state.isLogin ? 'logout' : 'login' }) 
  }

  return (
    <button onClick={clickEvent}>{state.isLogin ? 'login', 'logout'}</button>
  )
}

并发模式

  1. React18之前,渲染是一个单一的、不间断的、同步的事务,一旦渲染开始,就不能被中断
  2. React18引入并发模式,它允许你将更新作为一个transitions,这会告诉React他们可以被中断执行。这样可以把紧急的任务先更新你,不紧急的任务后更新

startTransition(API)

import { startTransition } from 'React'
const A = () => {
  ...
  const fun = () => {
    // 紧急任务
    setA('')
    // 不紧急任务(将内部的任何非紧急状态更新标记为 Transition)
    startTransition(() => setB(''))
  }
  return <div></div>
}

useTransition和useDeferredValue

  1. useTransition返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数
import { useTransition } from 'React'
const A = () => {
  ...
  const [pending, startTransition] = useTransition()
  const fun = () => {
    // 紧急任务
    setA('')
    // 不紧急任务(将内部的任何非紧急状态更新标记为 Transition)
    startTransition(() => setB(''))
  }
  return <div></div>
}
  1. useDeferredValue接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后
import { useDeferredValue, useState } from 'React'
const A = () => {
  const [a, setA] = useState('')
  // aD => 不紧急时候的值(同a),也就是延迟之后的值
  const aD = useDeferredValue(a)
  ...
  return <div></div>
}

自定义hook

  1. 命名通常使用use开头
  2. eg:
import { useState, useEffect } from 'React'

const useMouseXY = () => {
  const [x, setX] = usestate(0)
  const [y, setY] = usestate(0)

  useEffect(() => {
    function move(e) {
      setX(e.pageX)
      setY(e.pageY)
    }
    document.addEventListener('mousemove', move)
    return () => document.removeEventListener('mousemove', move)
  }, [])
  
  return { x, y }
}

// use 
const { x, y } = useMouseXY()

Suspense

组件加载中或是切换过程中的“中间态”

import { Suspense, useState } from 'react';
import { RouterProvider } from 'react-router-dom';
import router from '@/router/index';
import Loading from '@/components/Loading';

function MyApp() {
  const [show, setShow] = useState(true)
  const clickEvent = () => setShow(!show)
  return (
    <>
      <button onClick={clickEvent}>btn</button>
      <Suspense fallback={<Loading />}>
        {show ? <RouterProvider router={router} /> : <>other template</>}
      </Suspense>
    </>
  );
}

export default MyApp;

结合startTransition使用:

如果需要切换过程中不展示loading的加载态,则将上述代码中的切换逻辑转为并发异步即可

const clickEvent = () => startTransition(() => setShow(!show))

错误边界捕获

如果编写的组件内部报错,那么react渲染会清除根节点DOM,React官方文档暂时只有类组件编写的示例代码。其核心逻辑在类组件上支持关键的生命周期方法getDerivedStateFromError()和componentDidCatch(),无法将错误边界编写为函数式组件,同时以下几种错误情况,也捕获不到:

  1. 异步代码
  2. 事件处理函数
  3. 服务器组件
  4. Error Boundary自身

具体的完整封装 TODO:

🎈ReactDOM

createPortal

  1. 可以指定节点挂载到指定目标DOM节点
// template
const A = () => ReactDOM.createPortal(<div>info</div>, document.body)
  1. 对于一些全局组件(message),DOM的挂在可能不在root根节点内,自定义挂在指定DOM节点,除了上述createPortal方式,还有react的createRoot方式

flushSync

import { useState } from 'React'
import { flushSync } from 'ReactDDOM'
const A = () => {
  const [a, setA] = useState(1)
  const [b, setB] = useState('b')
  const addA = () => {
    setA(a + 1)
    setA(a + 1)
    setA(a + 1)
  }
  flushSync(() => {
    setA(a + 1)
  })
  flushSync(() => {
    setB('bb')
  })
  return <div onClick={addA}>a value: {a}</div>
}

🎈Others

hooks

  1. use:react18中的处理异步资源的一种方式,可解析Promise等,使用前提为服务端渲染或者开启异步的客户端
  2. useDebugValue
  3. useId:生成唯一id,比如一个页面需要调用两个相同组件,并且需要给各自组件一个唯一id,useId就很适用
  4. useImperativeHandle
  5. useInsertionEffect:适用于CSS-in-JS相关场景
  6. useOptimistic:过渡态,有点类似Vue中的Suspense
  7. useSyncExternalStore

组件

  1. <Profiler>:测量组件渲染性能
  2. <StrictMode>:严格模式,常用于开发环境再开发过程中就发现错误,但是会多执行组件渲染一次

评论区

什么都不舍弃,什么也改变不了