You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constonCheckedAllChange=newCheckedAll=>{// 构造新的勾选map
let newCheckedMap: CheckedMap={}// 全选if(newCheckedAll){cartData.forEach(cartItem=>{newCheckedMap[cartItem.id]=true})}// 取消全选的话 直接把map赋值为空对象setCheckedMap(newCheckedMap)}
如果是
全选 就把checkedMap的每一个商品id都赋值为true。
反选 就把checkedMap赋值为空对象。
渲染商品子组件
{cartData.map(cartItem=>{const{ id }=cartItemconstchecked=checkedMap[id]return(<ItemCardkey={id}cartItem={cartItem}checked={checked}onCheckedChange={onCheckedChange}/>)})}
前言
本文由一个基础的购物车需求展开,一步一步带你深入理解React Hook中的坑和优化
通过本篇文章你可以学到:
✨React Hook + TypeScript编写
业务组件
的实践✨如何利用React.memo
优化性能
✨如何避免Hook带来的
闭包陷阱
✨如何抽象出简单好用的
自定义hook
预览地址
https://sl1673495.github.io/react-cart
代码仓库
本文涉及到的代码已经整理到github仓库中,用cra搭建了一个示例工程,关于性能优化的部分可以打开控制台查看重渲染的情况。
https://github.com/sl1673495/react-cart
需求分解
作为一个购物车需求,那么它必然涉及到几个需求点:
需求实现
获取数据
首先我们请求到购物车数据,这里并不是本文的重点,可以通过自定义请求hook实现,也可以通过普通的useState + useEffect实现。
勾选逻辑实现
我们考虑用一个对象作为映射表,通过
checkedMap
这个变量来记录所有被勾选的商品id:计算勾选总价
再用reduce来实现一个计算价格总和的函数
那么此时就需要一个过滤出所有选中商品的函数
最后把这俩函数一组合,价格就出来了:
有人可能疑惑,为什么一个简单的逻辑要抽出这么几个函数,这里我要解释一下,为了保证文章的易读性,我把真实需求做了简化。
在真实需求中,可能会对不同类型的商品分别做总价计算,因此
filterChecked
这个函数就不可或缺了,filterChecked可以传入一个额外的过滤参数,去返回勾选中的商品的子集
,这里就不再赘述。全选反选逻辑
有了
filterChecked
函数以后,我们也可以轻松的计算出派生状态checkedAll
,是否全选:写出全选和反全选的函数:
如果是
全选
就把checkedMap
的每一个商品id都赋值为true。反选
就把checkedMap
赋值为空对象。渲染商品子组件
可以看出,是否勾选的逻辑就这样轻松的传给了子组件。
React.memo性能优化
到了这一步,基本的购物车需求已经实现了。
但是现在我们有了新的问题。
这是React的一个缺陷,默认情况下几乎没有任何性能优化。
我们来看一下动图演示:
购物车此时有5个商品,看控制台的打印,每次都是以5为倍数增长每点击一次checkbox,都会触发所有子组件的重新渲染。
如果我们有50个商品在购物车中,我们改了其中某一项的
checked
状态,也会导致50个子组件重新渲染。我们想到了一个api:
React.memo
,这个api基本等效于class组件中的shouldComponentUpdate
,如果我们用这个api让子组件只有在checked发生改变的时候再重新渲染呢?好,我们进入子组件的编写:
在这种优化策略下,我们认为只要前后两次渲染传入的props中的
checked
相等,那么就不去重新渲染子组件。React Hook的陈旧值导致的bug
到这里就完成了吗?其实,这里是有bug的。
我们来看一下bug还原:
如果我们先点击了第一个商品的勾选,再点击第二个商品的勾选,你会发现第一个商品的勾选状态没了。
在勾选了第一个商品后,我们此时的最新的
checkedMap
其实是而由于我们的优化策略,第二个商品在第一个商品勾选后没有重新渲染,
注意React的函数式组件,在每次渲染的时候都会重新执行,从而产生一个闭包环境。
所以第二个商品拿到的
onCheckedChange
还是前一次渲染购物车这个组件的函数闭包中的,那么checkedMap
自然也是上一次函数闭包中的最初的空对象。因此,第二个商品勾选后,没有按照预期的计算出正确的
checkedMap
而是计算出了错误的
这就导致了第一个商品的勾选状态被丢掉了。
这也是React Hook的闭包带来的臭名昭著陈旧值的问题。
那么此时有一个简单的解决方案,在父组件中用
React.useRef
把函数通过一个引用来传递给子组件。由于
ref
在React组件的整个生命周期中只存在一个引用,因此通过current永远是可以访问到引用中最新的函数值的,不会存在闭包陈旧值的问题。// 要把ref传给子组件 这样才能保证子组件能在不重新渲染的情况下拿到最新的函数引用 const onCheckedChangeRef = React.useRef(onCheckedChange) // 注意要在每次渲染后把ref中的引用指向当次渲染中最新的函数。 useEffect(() => { onCheckedChangeRef.current = onCheckedChange }) return ( <ItemCard key={id} cartItem={cartItem} checked={checked} + onCheckedChangeRef={onCheckedChangeRef} /> )
子组件
到此时,我们的简单的性能优化就完成了。
自定义hook之useChecked
那么下一个场景,又遇到这种全选反选类似的需求,难道我们再这样重复写一套吗?这是不可接受的,我们用自定义hook来抽象这些数据以及行为。
并且这次我们通过useReducer来避免闭包旧值的陷阱(dispatch在组件的生命周期中保持唯一引用,并且总是能操作到最新的值)。
这时候在组件内使用,就很简单了:
我们在自定义hook里把复杂的业务逻辑全部做掉了,包括数据更新后的无效id剔除等等。快去推广给团队的小伙伴,让他们早点下班吧。
自定义hook之useMap
有一天,突然又来了个需求,我们需要用一个map来根据购物车商品的id来记录另外的一些东西,我们突然发现,上面的自定义hook把map的处理等等逻辑也都打包进去了,我们只能给map的值设为
true / false
,灵活性不够。我们进一步把
useMap
也抽出来,然后让useCheckedMap
基于它之上开发。useMap
这是一个通用的map操作的自定义hook,它考虑了闭包陷阱,考虑了旧值的删除。
在此之上,我们实现上面的
useChecked
useChecked
总结
本文通过一个真实的购物车需求,一步一步的完成优化、踩坑,在这个过程中,我们对React Hook的优缺点一定也有了进一步的认识。
在利用自定义hook把通用逻辑抽取出来后,我们业务组件内的代码量大大的减少了,并且其他相似的场景都可以去复用。
React Hook带来了一种新的开发模式,但是也带来了一些陷阱,它是一把双刃剑,如果你能合理使用,那么它会给你带来很强大的力量。
感谢你的阅读,希望这篇文章可以给你启发。
The text was updated successfully, but these errors were encountered: