前言
React Hook 已成为当前最流行的开发范式,React 16.8 以后基于 Hook 开发极大简化开发者效率,同时不正确的使用 React Hook也带来了很多的性能问题,本文梳理基于 React Hook 开发组件的过程中如何提高性能。
组件抽取
优化前
每次点击 Increase 都会引起子组件 Child 的渲染,哪怕子组件并没有状态变化
function?Before(){ ????console.log('Demo1?Parent') ????let?[count,setCount]?=?useState(0) ????let?[name,setName]?=?useState('-') ????const?handleClick?=?()=>{ ????????setCount(count+1) ????} ????const?handleInput?=?(e)=>{ ????????setName(e.target.value) ????} ????return?( ????????<div> ????????????<div?className='l50'> ????????????????<label>计数器:</label> ????????????????<span?className='mr10'>{count}</span> ????????????????<button?className='ml10'?onClick={handleClick}>Increase</button> ????????????</div> ????????????<div?className='l50'> ????????????????<label?htmlFor="">改变子组件:</label> ????????????????<input?type="text"?onChange={handleInput}/> ????????????</div> ????????????<hr?/> ????????????<Child?name={name}/> ????????</div> ????) } //?子组件 function?Child(props){ ????console.log('Demo1?Child') ????return?( ????????<div?className='l50'> ????????????子组件渲染:{props.name} ????????</div> ????) }
优化后
只需要把 Increase 抽取成独立的组件即可。此时点击按钮,子组件并不会渲染。
/** ?*?优化后,Increase提取以后,上下文发生变化,组件内 ?*?@returns? ?*/ function?Increase(){ ????console.log('Child?Increase') ????let?[count,setCount]?=?useState(0) ????const?handleClick?=?()=>{ ????????setCount(count+1) ????} ????return?( ????????<div> ????????????<div?className='l50'> ????????????????<label>计数器:</label> ????????????????<span?className='mr10'>{count}</span> ????????????????<button?className='ml10'?onClick={handleClick}>Increase</button> ????????????</div> ????????</div> ????) } function?After(){ ????console.log('Demo1?Parent') ????let?[name,setName]?=?useState('-') ????const?handleInput?=?(e)=>{ ????????setName(e.target.value) ????} ????return?( ????????<div> ????????????<Increase/> ????????????<div?className='l50'> ????????????????<label?htmlFor="">改变子组件:</label> ????????????????<input?type="text"?onChange={handleInput}/> ????????????</div> ????????????<Child?name={name}/> ????????</div> ????) } //?子组件 function?Child(props){ ????console.log('Demo1?Child') ????return?( ????????<div?className='l50'> ????????????子组件渲染:{props.name} ????????</div> ????) }
memo 优化组件
同样基于上述优化前代码,如果不抽取组件,使用 memo 优化后,当点击按钮后,也不会触发二次渲染。
//?优化前 function?AfterMemo(){ ????console.log('Demo1?Parent') ????let?[count,setCount]?=?useState(0) ????let?[name,setName]?=?useState('-') ????const?handleClick?=?()=>{ ????????setCount(count+1) ????} ????const?handleInput?=?(e)=>{ ????????setName(e.target.value) ????} ????return?( ????????<div> ????????????<div?className='l50'> ????????????????<label>计数器:</label> ????????????????<span?className='mr10'>{count}</span> ????????????????<button?className='ml10'?onClick={handleClick}>Increase</button> ????????????</div> ????????????<div?className='l50'> ????????????????<label?htmlFor="">改变子组件:</label> ????????????????<input?type="text"?onChange={handleInput}/> ????????????</div> ????????????<Child?name={name}/> ????????</div> ????) } //?子组件 const?Child?=?memo((props)=>{ ????console.log('Demo1?Child') ????return?( ????????<div?className='l50'> ????????????子组件渲染:{props.name} ????????</div> ????) })
React.memo 语法
React.memo 为高阶组件,与 React.PureComponent相似。
function?TestComponent(props){ ??//?使用?props?渲染 } function?areEqual(prevProps,nextProps){ ??/* ??如果把?nextProps?传入?render?方法的返回结果与 ??将?prevProps?传入?render?方法的返回结果一致则返回?true, ??否则返回?false ??*/ } export?default?React.memo(TestComponent,areEqual)
与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。
useCallback 优化组件
如果已经用了 memo ,当遇到下面这种场景时,同样会触发子组件渲染。比如,给 Child 绑定一个 handleClick ,子组件内部增加一个按钮,当点击子组件的按钮时,更改 count 值,即使没有发生 name 变化,也同样会触发子组件渲染,为什么? memo 不是会判断 name 变化了,才会更新吗?
function?Before(){ ????console.log('Demo1?Parent') ????let?[count,setCount]?=?useState(0) ????let?[name,setName]?=?useState('-') ????const?handleClick?=?()=>{ ????????setCount(count+1) ????} ????const?handleInput?=?(e)=>{ ????????setName(e.target.value) ????} ????const?handleChange?=?()=>{ ????????setCount(count+1) ????} ????return?( ????????<div> ????????????<div?className='l50'> ????????????????<label>计数器:</label> ????????????????<span?className='mr10'>{count}</span> ????????????????<button?className='ml10'?onClick={handleClick}>Increase</button> ????????????</div> ????????????<div?className='l50'> ????????????????<label?htmlFor="">改变子组件:</label> ????????????????<input?type="text"?onChange={handleInput}/> ????????????</div> ????????????<Child?name={name}?handleClick={handleChange}/> ????????</div> ????) } //?子组件 const?Child?=?memo((props)=>{ ????console.log('Demo1?Child') ????return?( ????????<div?className='l50'> ????????????子组件渲染:{props.name} ????????????<button?onClick={props.handleClick}>更改count</button> ????????</div> ????) })
并不是 memo 没有生效,是因为当状态发生变化时,父组件会从新执行,导致从新创建了新的 handleChange 函数,而 handleChange 的变化导致了子组件的再次渲染。
优化后
点击父组件的 Increase 按钮,更改了 count 值,经过 useCallback 包裹 handleChange 函数以后,我们会发现子组件不再渲染,说明每当父组件执行的时候,并没有创建新的 handleChange 函数,这就是通过 useCallback 优化后的效果。 即使我们点击子组件的按钮,也同样不会触发子组件的渲染,同样 count 会进行累加。
function?After(){ ????console.log('Demo1?Parent') ????let?[count,setCount]?=?useState(0) ????let?text?=?useRef(); ????let?[name,setName]?=?useState('-') ????const?handleClick?=?()=>{ ????????setCount(count+1) ????} ????const?handleInput?=?(e)=>{ ????????setName(e.target.value) ????} ????const?handleChange?=?useCallback(()=>{ ????????//?为了让?count?能够累加,我们使用ref?获取值 ????????let?val?=?parseInt(text.current.textContent); ????????setCount(val+1) ????},[]) ????return?( ????????<div> ????????????<div?className='l50'> ????????????????<label>计数器:</label> ????????????????<span?className='mr10'?ref={text}>{count}</span> ????????????????<button?className='ml10'?onClick={handleClick}>Increase</button> ????????????</div> ????????????<div?className='l50'> ????????????????<label?htmlFor="">改变子组件:</label> ????????????????<input?type="text"?value={name}?onChange={handleInput}/> ????????????</div> ????????????<Child?name={name}?handleClick={handleChange}/> ????????</div> ????) }
useCallback 作用
//?用法 useCallback(()=>{ ??//?to-do },[]) //?示例 function?App(){ ??//?点击按钮调用此函数,但返回被缓存 ??const?onClick?=?useCallback(()?=>?{ ????console.log('我被缓存了,怎么点击都返回一样'); ??},?[]); ??return?(? ????<button?onClick={onClick}>点击</button> ??); }useCallback 接收 2 个参数,第一个为缓存的函数,第二个为依赖值 主要用于缓存函数,第二次会返回同样的结果。
useMemo 优化
我们定义了一个 total 函数,内部使用 1 填充了100次,通过 reduce 计算总和,经过测试发现点击 Increase 按钮后,只会执行 total1 ,不会执行 total2 ,假设 total 计算量巨大,就会造成内存的浪费,通过 useMemo 可以帮我们缓存计算值。
function?Before(){ ????console.log('Demo1?Parent') ????let?[count,setCount]?=?useState(0) ????const?handleClick?=?()=>{ ????????setCount(count+1) ????} ????const?total1?=?()=>{ ????????console.log('计算求和1') ????????let?arr?=?Array.from({?length:100?}).fill(1) ????????return?arr.reduce((prev,next)=>prev+next,0) ????} ????//?缓存对象值 ????const?total2?=?useMemo(()=>{ ????????console.log('计算求和2') ????????let?arr?=?Array.from({?length:100?}).fill(1) ????????return?arr.reduce((prev,next)=>prev+next,0) ????},[count]) ????return?( ????????<div> ????????????<div?className='l50'> ????????????????<label>计数器:</label> ????????????????<span?className='mr10'>{count}</span> ????????????????<button?className='ml10'?onClick={handleClick}>Increase</button> ????????????</div> ????????????<div> ????????????????<label>总和:</label> ????????????????<span>{total1()}</span> ????????????????<span>{total2}</span> ????????????</div> ????????</div> ????) }
useMemo 语法
const?memoizedValue?=?useMemo(()?=>?computeExpensiveValue(a,?b),?[a,?b]);传入一个函数进去,会返回一个 memoized 值,需要注意的是,函数内必须有返回值 第二个参数会依赖值,当依赖值更新时,会从新计算。
useCallback 和 useMemo 区别
他们都用于缓存, useCallback 主要用于缓存函数,返回一个 缓存后 函数,而 useMemo 主要用于缓存值,返回一个缓存后的值。
到此这篇关于React Hook 四种组件优化总结的文章就介绍到这了,更多相关React Hook 组件优化内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
查看更多关于React Hook 四种组件优化总结的详细内容...