求求你们了!react项目 这个优化不做不行啊!
前言
一言以蔽之,这是一个常见面试题:减少子组件优化方案。面试官会说: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的痛点
接着说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的原理
如上图,我们从左往右看,首先最左侧是一个数据结构,类似
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就可以了。
本文结束!
转载自:https://juejin.cn/post/7081580874843553805