谈到React优化,估计说的最多的就是减少子组件渲染,减少真实DOM操作等。
一 减少渲染
1. shouldComponentUpdate
通过对Props和State的浅比较,如果没有变化,return false,避免重复多余的render方法调用,省去虚拟DOM的生成和对比过程,提高性能。
早期类似的技术有pureRender,16版本中可以直接让class组件继承PureComponent,它的实现其实很简单,只是做浅比较,因为过深的比较也会消耗很多的时间,或许还比render方法带来的消耗大,所以这其实是权衡和取舍。
注意这里的浅比较,基础类型比较值是否相等,不等则需要再次渲染;对于引用类型,先比较引用的地址,地址相同,不渲染,地址不同,判断两个对象的keys的长度,长度不等,则需要渲染,相等则对第一层属性比较,如果有不同,则渲染,否则不渲染。下面的code则是核心主要逻辑。
注意object.is(ES6推出的)和===的主要区别,是object.is(+0,-0) === false, Object.is(NaN,Nan) === false
function is(x, y) { //处理了基本类型的比较 //1,针对+0===-0的情况 //2. 针对NaN!==NanN的情况 return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y ; } var is$1 = typeof Object.is === 'function' ? Object.is : is; var hasOwnProperty$2 = Object.prototype.hasOwnProperty; //返回值:false更新 true不更新 function shallowEqual(objA, objB) { if (is$1(objA, objB)) {//基本数据类型 不更新 return true; } //由于Object.is 可以对基本数据类型做一个精确的比较 如果不等只有一种情况 那就是object, objA/objB中,只要有一个不是object或为null则返回false if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } //过滤掉基本数据类型 就是对象比较 首先比较长度 优化性能 //比较oldProps和新的Props以及oldState和newState长度是否相同 如果长度不同则重新更新渲染 如果长度相同则不用重新渲染 如果不相等 不会往下执行 优化性能 var keysA = Object.keys(objA); var keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } //如果key相等 //如果objA的属性不在objB里,或者是objA的属性值和objB的属性值不等 则重新渲染 不考虑当keysA[i]为对象的多层问题 浅显比较 提高性能 for (var i = 0; i < keysA.length; i++) { if (!hasOwnProperty$2.call(objB, keysA[i]) || !is$1(objA[keysA[i]], objB[keysA[i]])) { return false; } return true; } }
2. React.memo()
由于PureComponent是只能用于class组件的继承,react为了支持函数组件,所以又推出了一个高级组件React.memo(), 当props变化时做浅比较。注意如果用useState或者useContext等hooks导致state状态变化时,函数组件依然会重新渲染,否则复用上一次的渲染结果(复用上一次生成的虚拟子节点Dom)。
const Child = memo(props => { useEffect(() => { console.log("schema", props.schema); }, [JSON.stringify(props.schema)]); return <div>Child</div>; }); //another case const ChildMedo = React.memo(() => { return <Child1 step={step} count={count} /> });
3. useMemo
用于函数组件,减少计算一些中间值,废话别太多,Show you code。
export default function useMemoDemo() { const [count, setCount] = useState(1); const [val, setValue] = useState(''); // 使用useMemo的话 当input的值改变了,count没有变化,func 还是拿到之前的缓存的值,如果计算过程很复杂,就节省了性能的再次计算的开销 const func= useMemo(() => { let result = Math.random() * count; return result; }, [count]); return <div> <h4>{count}-{func}</h4> {val} <div> <button onClick={() => setCount(count + 1)}>+c1</button> <input value={val} onChange={event => setValue(event.target.value)}/> </div> </div>; }
4. redux中的优化
对于 Redux 来说,每当 store 发生改变时,所有的 connect 都会重新计算。在一个大型应用中,浪费的重复计算可想而知。为了减少性能浪费, 我们想到对 connect 中的这些 selector函数做缓存。
Redux 拥抱了函数式编程,而在函数式编程中,纯函数的众多好处之一就是方便做缓存。那么,如何用纯函数做缓存呢?在数学上,如果自变量不变,因变量总是不变。同样,用相同的参数执行纯函数多次,每次返回的结果一定相同。也就是说,如果纯函数的参数不变的话,可以把之前用同样的参数计算出来的结果直接返回。 (摘自《深入React技术栈》)
我们可以直接使用reselect库帮我们完成。
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) { let lastArgs = null let lastResult = null return (...args) => { if ( lastArgs !== null && lastArgs.length === args.length && args.every((value, index) => equalityCheck(value, lastArgs[index])) ) { return lastResult } lastResult = func(...args) lastArgs = args return lastResult } }
defaultMemoize 函数运用了闭包的原理,使纯函数的参数和结果缓存在内存中。为了让
defaultMemoize 函数中缓存的数据常驻内存,我们需要让 defaultMemoize 处于全局作用域,或者
用其他作用域链连接到全局作用域。
5.高阶reducer分析出哪些reducer或action耗费的时间太长,预警, 这样之后就可以有针对性优化。
export default function logSlowReducers(reducers, thresholdInMs = 8) { Object.keys(reducers).forEach((name) => { const reducer = reducers[name]; // 将每个 reducer 用高阶函数包装 reducers[name] = (state, action) => { const start = Date.now(); const result = originalReducer(state, action); const diffInMs = Date.now() - start; if (diffInMs >= thresholdInMs) { console.warn(`Reducer ${name} took ${diffInMs} ms for ${action.type}`); } return result; }; }); return reducers; }
6. 特定的action
在 Redux 中,每个 action 被分发,所有的 reducer 都会被执行一次。虽然每个 reducer 仅仅只
是执行一个 switch 判断,但所有的 reducer 加起来的执行时间也不容小觑。
大多数情况下,应用的 action 都是和某个 reducer 对应。因此,我们可以指定特殊情况,让
Redux 在特殊情况之外只执行与 action 对应的那个 reducer 。
7. 合并多个action
比如有5个同步action,依次触发,每一个action都需要一个很大的redux流程,以及dom树的更新,我们可以通过一些办法合并他们,只产生一个最终的state,最后再一次性更新组件树。
8. 提高编程技巧,避免属性每次都传入一个新对象
比如字面量prop = {a:b},每次都会产生要给新的对象
比如我们经常使用的合成事件的绑定。()=> {}, 或者onchange.bind(this), 最好存储为类的成员属性做缓存。
9. 其他
使用redux标准接入组件方式,connect方法的第4个参数options={pure:true},默认也会对传入属性做浅比较,避免被包装组件重复不必要的渲染。
其实还有很多。
二 刻意去迎合diff算法
1. 同级别的元素使用唯一key,减少diff比较,增强复用,比如使用绑定元素的id。
2. 避免(组件结构)节点的跳跃变动,因为diff算法只会同级别比较,一旦发现不同就会大刀阔斧地剪枝或增枝。
3. 同级别节点中,避免因为代码问题,产生太多的移动。
比如有5个兄弟元素,只有最后一个元素(索引为4)调到第一个位置来,其他节点索引都变大1位,react的diff算法是,索引变小不移动,索引变大需要移动,so,这就会增加出其余4个元素向后移动4次真实 DOM操作。
当然react的diff算法的时间复杂度已经从O(n^3)优化到 O(n),从2/8原则的角度来看,绝大多数场景的性能已经很棒了。