困惑
写过react项目的同学,都经历过react性能优化,一般的方法是缓存某些计算,或者使用purecomponent、memo等方法减少不必要的组件渲染,以及使用context替代props透传等,下面探讨一种能从开始开发项目时就能保证页面性能的方法。
常用弊端方法
1.props透传
大多数的项目堆砌react代码时,都采用props一层层传递的方式,拼凑出一大堆组件代码,等到发现性能问题时再去查找瓶颈的原因。
任意位置父节点的渲染都会导致子孙节点的渲染,这些渲染有必要的也有非必要的,非必要的渲染就是导致性能危机的地方。
叶子节点的渲染可能依赖于某个props,这就需要props传递不被阻断,性能很难优化。
2.pureComponent、shouldComponentUpdate
这两种方式可以用来对比props的变化,即使祖先渲染了而自己的props非改变时就不渲染,节省性能,但这样也带来了其他的问题
2.1diff性能损失
这两种方式是通过diff比较来判断props是否变化的,而有些情况下由于写法等原因,props是必然变化的,就导致了盲目的使用不仅没有带来性能提升,反而多出了一部分diff计算时间
2.2叶子节点
如果用在某个父元素上,即使这个父元素不需要渲染,但是后面的子孙节点需要基于某个props来改变渲染,所以也必然要通过这个父元素的重新渲染来达到目的
总而言之,使用这两种方法并不能精确控制所有的节点渲染必要性,可能使性能优化变得更加棘手
3.memo
使用hooks的同学只能通过memo方法来判断组件是否需要渲染,使用memo在复杂场景下需要第二个参数的判断,弊端与上面类似,而且还会遇到一些该渲染而未渲染的坑点
4.useCallback、useMemo
配合memo等diff计算非必要渲染的手段,将props缓存起来,就是上面提到的pureComponent在有些情况下性能会变的更糟,原因就是在写法上没有缓存的话,就会浪费diff计算时间,例如
<Input onChange={e=>{setValue(e.target.value)}} />
由于onchange的属性是个匿名函数,每次组件渲染时,input传入的props都会变,就会导致memo、pureComponent等优化失效。所以请将传入组件的props使用useCallback或useMemo包裹起来,让props非必要不改变。
但是在antd组件中,没有使用memo等包裹组件,即使传入antd的props使用了useCallback、useMemo,也不能带来性能提升。
但是我们自己开发时封装的组件是需要做性能优化的,在传入这类组件的props时,需要使用useCallback或useMemo包装。
5.context
context方法是比较简单的性能优化策略,大量减少props的透传,再配合useContext的使用,写法变得非常简洁。
但是context也没法做到精确控制渲染的必要性,因为组件订阅了context后只要context中某个值发生变化,即使没有使用这个值也会导致组件重新渲染。
6.解决方法
6.1使用rxjs精确控制组件渲染时机
先不了解rxjs具体的概念,把rxjs当做是eventEmitters订阅工具,先手写两个方法eventInput(输入)、eventOutput(输出),并且把这两个方法放到context中,所有想要订阅eventOutput的组件,就使用useContext获取并且监听eventOutput事件,由于eventInput、eventOutput都是静态不变的函数,这就保证了context中的value不会变化(context不存放变化的value值),变化的始终是event中的流。达到的效果就是精确控制任意想变化的组件。
上图中,子孙节点也能控制任意位置的祖先节点的渲染,而不改变其他组件的渲染
6.2memo包裹
父组件重新渲染必然导致后续子组件的渲染,所以使用React.memo包裹每个组件。
这里有个原则就是组件之间尽量不传递props,只使用rxjs订阅需要的值。
这样就保证了可以放心使用memo,而不担心是否有多余的diff和未知的坑点。所有组件没有props传递,只关心自己订阅的值,只有自己订阅的值改变了才去渲染,做到手术刀式的控制渲染。
6.3副作用分离出ui
eventInput、eventOutput从输入到输出,中间是可以设定过程的,把ui中的所有副作用或计算都可以放到这些过程中去,既可以保证ui文件体积减小,也可以让关注点分离,ui只做跟渲染有关的事情。举个例子:
--a.tsx
eventInput({id:1})
--hooks.ts
eventOutput.pipe(({id})=>{
const res = await axios.get(id)
const colors = res.map(v=>v.color)
return colors
})
--b.tsx
eventOutput.sub(colors=>{
setState(colors)
})
7.活生生的例子
provider.jsx
import {useFetchResult,usePeriodChange} from 'useData.js'
export const context = React.createContext(({}))
const Provider = ({ children }) => {
const { fetchResultInput$,fetchResultOutput$ } = useFetchResult()
const { periodChangeInput$ } = usePeriodChange(fetchResultInput$)
return (
<context.Provider
value={{
fetchResultOutput$,
periodChangeInput$,
}}
>
{children}
</context.Provider>
)
}
export default Provider
useData.js
export const usePeriodChange = (fetchResultInput$) => {
const {periodChangeInput$} = useMemo(() => {
const periodChangeInput$ = new Subject()
return { periodChangeInput$ }
}, [])
useEffect(()=>{
const sub = periodChangeInput$.subscribe(period=>{
const params = {...period,appid:123}
fetchResultInput$.next(params)
})
return ()=>sub()
},[fetchResultInput$,periodChangeInput$])
return {periodChangeInput$}
}
export const useFetchResult = (fetchResultInput$) => {
return useMemo(() => {
const fetchResultInput$ = new Subject()
const fetchResultOutput$ = fetchResultInput$.pipe(
switchMap(params=>{
return axios.get('/',params)
}),
map(res=>{
...
calc(res)
...
return result
})
)
return { fetchResultInput$, fetchResultOutput$ }
}, [])
}
index.jsx
import Provider from './provider.jsx'
export default () => (
<Provider>
<Panel />
</Provider>
)
panel.jsx
export default () => (
<>
<Period />
<Table />
</>
)
preiod.jsx
export default () => {
const {periodChangeInput$} = useContext(context)
const onChange = useCallback((period)=>{
periodChangeInput$.next(period)
},[periodChangeInput$])
return <Select onChange={onChange} />
}
table.jsx
export default () => {
const [data,setData] = useState([])
const {fetchResultOutput$} = useContext(context)
useEffect(()=>{
const sub = fetchResultOutput$.subscribe(data=>setData(data))
return ()=>sub()
},[fetchResultOutput$])
return <Table data={data} />
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。