4

困惑

写过react项目的同学,都经历过react性能优化,一般的方法是缓存某些计算,或者使用purecomponent、memo等方法减少不必要的组件渲染,以及使用context替代props透传等,下面探讨一种能从开始开发项目时就能保证页面性能的方法。

常用弊端方法

1.props透传

大多数的项目堆砌react代码时,都采用props一层层传递的方式,拼凑出一大堆组件代码,等到发现性能问题时再去查找瓶颈的原因。
未命名文件.png

任意位置父节点的渲染都会导致子孙节点的渲染,这些渲染有必要的也有非必要的,非必要的渲染就是导致性能危机的地方。
叶子节点的渲染可能依赖于某个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中某个值发生变化,即使没有使用这个值也会导致组件重新渲染。
未命名文件 (1).png

6.解决方法

6.1使用rxjs精确控制组件渲染时机

先不了解rxjs具体的概念,把rxjs当做是eventEmitters订阅工具,先手写两个方法eventInput(输入)、eventOutput(输出),并且把这两个方法放到context中,所有想要订阅eventOutput的组件,就使用useContext获取并且监听eventOutput事件,由于eventInput、eventOutput都是静态不变的函数,这就保证了context中的value不会变化(context不存放变化的value值),变化的始终是event中的流。达到的效果就是精确控制任意想变化的组件。
未命名文件 (2).png
上图中,子孙节点也能控制任意位置的祖先节点的渲染,而不改变其他组件的渲染

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

石坚
413 声望14 粉丝