官方文档:React 官方中文文档
📌变量相关
useState
普通定义的变量每次组件渲染都是所定义的值,如果我们需要在每次re-render都记住一个状态的最新值,那就需要使用useState hook
import { useState } from 'React'
const A = () => {
const [a, setA] = useState('a')
return <div>a value: {a}</div>
}
和类组件的差异
- 改变值的时候不会merge,而是覆盖
- 初始值可以是回调函数,但不能是函数否则每次改变都会重新执行一遍该函数,影响性能
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>
);
};
- 同样存在批处理
【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>
);
};
- 传入的是值,渲染前会存在覆盖情况,如果传入的是函数,则会保留上一次的状态值计算
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
模拟生命周期/缓存变量
利用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
渲染完成再执行
- 模拟各个生命周期的执行时机
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>
);
};
- 关于第二个数组参数
- 如果不设置,那么在update时期都会执行
- 如果设置空数组(且回调函数中没有响应式变量),则只在mount时期执行一次
- 如果useEffect回调函数中存在响应式变量,那么第二个参数数组中应当有该响应式变量
useLayoutEffect
- useEffect:渲染并绘制到屏幕之后执行,异步
- useLayoutEffect:渲染之后,但绘制到屏幕之前执行,同步
- 一般如果回调函数中有DOM的相关操作并且会改变样式,用后者,避免DOM渲染闪屏/白屏,但前者性能方面更好
✨渲染优化
首先明确react组件重渲染的触发条件:
- props或者state发生改变触发组件重渲染
- 父组件的重渲染触发子组件的重渲染
参考:
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
性能优化组件
- 对传入的 props 进行浅比较,如果 props 没有变化,则不会重新渲染组件
- 函数组件中当响应变量的值没有发生改变,不会重新渲染,和类组件不一样
- 当组件的值发生改变才进行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 会重新创建这些匿名函数和对象。这些新创建的函数和对象在内存中是不同的,即使它们的内容相同。这会导致以下问题:
- 导致子组件不必要的重新渲染: 当子组件接收到新的匿名函数或对象作为 props 时,即使这些 props 的内容没有变化,React 仍然认为这些 props 发生了变化,因为它们是新的引用,进而导致子组件重新渲染。
- 性能问题: 频繁创建新的函数和对象会增加内存使用和垃圾回收的负担,尤其是在组件频繁更新的情况下,这会对性能产生负面影响。
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打印
- 两者的第二个参数都是依赖数组,同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>
)
}
并发模式
- React18之前,渲染是一个单一的、不间断的、同步的事务,一旦渲染开始,就不能被中断
- React18引入并发模式,它允许你将更新作为一个transitions,这会告诉React他们可以被中断执行。这样可以把紧急的任务先更新你,不紧急的任务后更新
startTransition(API)
import { startTransition } from 'React'
const A = () => {
...
const fun = () => {
// 紧急任务
setA('')
// 不紧急任务(将内部的任何非紧急状态更新标记为 Transition)
startTransition(() => setB(''))
}
return <div></div>
}
useTransition和useDeferredValue
- useTransition返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数
import { useTransition } from 'React'
const A = () => {
...
const [pending, startTransition] = useTransition()
const fun = () => {
// 紧急任务
setA('')
// 不紧急任务(将内部的任何非紧急状态更新标记为 Transition)
startTransition(() => setB(''))
}
return <div></div>
}
- useDeferredValue接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后
import { useDeferredValue, useState } from 'React'
const A = () => {
const [a, setA] = useState('')
// aD => 不紧急时候的值(同a),也就是延迟之后的值
const aD = useDeferredValue(a)
...
return <div></div>
}
自定义hook
- 命名通常使用use开头
- 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(),无法将错误边界编写为函数式组件,同时以下几种错误情况,也捕获不到:
- 异步代码
- 事件处理函数
- 服务器组件
- Error Boundary自身
具体的完整封装 TODO:
🎈ReactDOM
createPortal
- 可以指定节点挂载到指定目标DOM节点
// template
const A = () => ReactDOM.createPortal(<div>info</div>, document.body)
- 对于一些全局组件(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
- use:react18中的处理异步资源的一种方式,可解析Promise等,使用前提为服务端渲染或者开启异步的客户端
- useDebugValue
- useId:生成唯一id,比如一个页面需要调用两个相同组件,并且需要给各自组件一个唯一id,useId就很适用
- useImperativeHandle
- useInsertionEffect:适用于CSS-in-JS相关场景
- useOptimistic:过渡态,有点类似Vue中的Suspense
- useSyncExternalStore
组件
- <Profiler>:测量组件渲染性能
- <StrictMode>:严格模式,常用于开发环境再开发过程中就发现错误,但是会多执行组件渲染一次
评论区