求求你们了!react项目 这个优化不做不行啊!

9,422 阅读4分钟

前言

一言以蔽之,这是一个常见面试题:减少子组件优化方案。面试官会说:immutable.js,太重,React.memo浅比较没啥用,定制化写比较函数太麻烦,你还有别的方法吗?

你们的react项目,有多少根本没有理会React.memo,useMemo,useCallBack的,造成大量子页面数据明明没有变化,却一直跟着刷新。

我以前在成都滴滴的项目,这种无用render的次数,各个子组件加起来,普通页面至少都是20次起步,复杂的页面,估计60次无用刷新都不为过,再加上浏览器页面本身的重绘和回流,这种前端页面不就是垃圾吗?

现在有人会分两种声音说:

  • 这个React.memo难用啊,它默认的浅比较,没啥用,很多数据是引用类型。或者自己写比较函数太麻烦了,业务太急,懒得写。况且复杂的数据,更难。
  • 也有人会说,那就要引进immutable.js了,那个东西贼难用,而且对于老项目来说侵入性太强了。

给新人解释为啥渲染次数多

最典型的案例:

const ChildComponent = React.memo(({ changeText }) => {
  console.log('子组件执行了');
  return (
      <p>test</p>
  )
});

const ParentComponent = () => {
  const [number, setNumber] = useState<number>(1);

  const handleChange = () => {
    setNumber(number + 1);
  };

  const changeText = (newText) => {
    setText(newText);
  };

  return (
    <>
      <button onClick={handleChange}>click me</button>
      <p>count: {number}</p>
      <ChildComponent changeText={changeText}  />
    </>
  )
};

每次点击按钮click me的时候,ChildComponent都会渲染,为啥呢?子组件不是都包裹了 React.memo吗,原因是changeText是一个引用类型,每次setNumber的时候,都会重新创建一个引用。

这个跟啥很类似呢,你是不是经常把style这么写

<组件A style={{ xx属性 }}>

每次也会生成新的对象,跟上面是一样的,所以这些写的人,完全就是实习生水平。

好了,一个子组件你说就多渲染一次有啥呢,但是你的项目肯定会有N个组件的嵌套是吧,是不是你组件嵌套的足够多,页面上组件足够多,可能无用的渲染就越多呢?

redux和useState的痛点

redux使用的一个超级不好的店,就是每次返回新的state,为啥呢,首先redux不要直接用到项目里,太难用了(模板代码多,来回切换文件烦死人),建议用dva,或者我们部门也有自研的解决方案,类似dva,但是拓展机制比dva更合理,源码分析地址如下:juejin.cn/post/708144…

接着说reudx使用的问题,这也是useState的问题,比如说你们的reducer应该都是这么写的

import * as constant from '../configs/action';

const initialState = {
    number: 0,
};

export default (state = initialState, action) => {
    switch (action.type) {
        case constant.INCREMENT:
            return {
                ...state,
                number: state.number + 1,
            };
        case constant.DECREMENT:
            return {
                ...state,
                number: state.number - 1,
            };
        case constant.CLEAR_NUM:
            return {
                ...state,
                number: 0,
            };
        default:
            return state;
    }
};

关键点在这里:

{
   ...state,
   number: state.number + 1,
}

首先state要自己浅复制一下,然后后面再重写属性,遇到嵌套对象,写起来恶心的要命。

useState({ a: 1, b:{ c:1 } })

如上,useState这种也是一个道理。

解决思路

当然,这是抛砖引玉,我们目前的解决思路很简单,React.memo,useMemo,useCallBack + immer(有immerHooks的包)

不可能变数据immer是一个可持久化数据结构的库,作者是mobx的作者,这个比immutable.js的侵入性小的多得多。

首先说下immer的原理

image.png 如上图,我们从左往右看,首先最左侧是一个数据结构,类似

const a = {
    b: {
        c: 1,
        d: 1,
        f: 1
    }
    g: { h: { i: 1 } } 
}

我们把属性a.g.h改动了,就如最左边红色部分,被改动了。然后immer就会把改动的那一根链条重新生成新的数据,如图绿色部分,然后蓝色哪些不变的数据,引用还是以前的不变。

为啥能解决之前说的问题呢,首先,我们看看怎么解决redux和useStae嵌套多层对象的问题

我们拿hooks, useImmerReducer来说

import React from "react";
import { useImmerReducer } from "use-immer";
 
const initialState = { count: 0 };
 
function reducer(draft, action) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return draft.count++;
    case "decrement":
      return draft.count--;
  }
}
 
function Counter() {
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

immer具体使用大家网上去搜,一大堆,我们这篇文章主要是为了提醒那些不做react基础优化的同学,醒醒吧海燕!!!

如上面代码,当你dispatch的时候,draft就是一个代理对象,当你draft.count++的时候,就像我们之前的红色节点一样,immer已经帮你做好了转化,生成新的对象,就跟之前图里的绿色节点+蓝色节点一样。

仅仅是immer能解决问题吗?

答案是不能,还记得我们之前说的案例吗,如下

const ChildComponent = React.memo(({ changeText }) => {
  console.log('子组件执行了');
  return (
      <p>test</p>
  )
});

const ParentComponent = () => {
  const [number, setNumber] = useState<number>(1);

  const handleChange = () => {
    setNumber(number + 1);
  };

  const changeText = (newText) => {
    setText(newText);
  };

  return (
    <>
      <button onClick={handleChange}>click me</button>
      <p>count: {number}</p>
      <ChildComponent changeText={changeText}  />
    </>
  )
};

这种函数数据持久化,需要用useCallBack,如下:

 const changeText = useCallback((newText) => {
    setText(newText);
  }, []);

还有一些可能需要useMemo,然后几乎所有组件都包裹一层React.memo就可以了。

本文结束!