前言
本文由一个根底的购物车需要开展,一步一步带你深刻了解 React Hook 中的坑和优化
通过本篇文章你能够学到:
✨React Hook + TypeScript 编写业务组件
的实际
✨ 如何利用 React.memo优化性能
✨ 如何防止 Hook 带来的闭包陷阱
✨ 如何形象出简略好用的自定义hook
预览地址
https://sl1673495.github.io/r...
代码仓库
本文波及到的代码曾经整顿到 github 仓库中,用 cra 搭建了一个示例工程,对于性能优化的局部能够关上控制台查看重渲染的状况。
https://github.com/sl1673495/...
需要合成
作为一个购物车需要,那么它必然波及到几个需要点:
- 勾选、全选与反选。
- 依据选中项计算总价。
需要实现
获取数据
首先咱们申请到购物车数据,这里并不是本文的重点,能够通过自定义申请 hook 实现,也能够通过一般的 useState + useEffect 实现。
const getCart = () => { return axios('/api/cart')}const { // 购物车数据 cartData, // 从新申请数据的办法 refresh,} = useRequest < CartResponse > getCart
勾选逻辑实现
咱们思考用一个对象作为映射表,通过checkedMap
这个变量来记录所有被勾选的商品 id:
type CheckedMap = { [id: number]: boolean,}// 商品勾选const [checkedMap, setCheckedMap] = useState < CheckedMap > {}const onCheckedChange: OnCheckedChange = (cartItem, checked) => { const { id } = cartItem const newCheckedMap = Object.assign({}, checkedMap, { [id]: checked, }) setCheckedMap(newCheckedMap)}
计算勾选总价
再用 reduce 来实现一个计算价格总和的函数
// cartItems的积分总和const sumPrice = (cartItems: CartItem[]) => { return cartItems.reduce((sum, cur) => sum + cur.price, 0)}
那么此时就须要一个过滤出所有选中商品的函数
// 返回已选中的所有cartItemsconst filterChecked = () => { return ( Object.entries(checkedMap) // 通过这个filter 筛选出所有checked状态为true的项 .filter((entries) => Boolean(entries[1])) // 再从cartData中依据id来map出选中列表 .map(([checkedId]) => cartData.find(({ id }) => id === Number(checkedId))) )}
最初把这俩函数一组合,价格就进去了:
// 计算礼享积分const calcPrice = () => { return sumPrice(filterChecked())}
有人可能纳闷,为什么一个简略的逻辑要抽出这么几个函数,这里我要解释一下,为了保障文章的易读性,我把实在需要做了简化。
在实在需要中,可能会对不同类型的商品别离做总价计算,因而filterChecked
这个函数就不可或缺了,filterChecked 能够传入一个额定的过滤参数,去返回勾选中的商品的子集
,这里就不再赘述。
全选反选逻辑
有了filterChecked
函数当前,咱们也能够轻松的计算出派生状态checkedAll
,是否全选:
// 全选const checkedAll = cartData.length !== 0 && filterChecked().length === cartData.length
写出全选和反全选的函数:
const onCheckedAllChange = (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 } = cartItem const checked = checkedMap[id] return ( <ItemCard key={id} cartItem={cartItem} checked={checked} onCheckedChange={onCheckedChange} /> ) })}
能够看出,是否勾选的逻辑就这样轻松的传给了子组件。
React.memo 性能优化
到了这一步,根本的购物车需要曾经实现了。
然而当初咱们有了新的问题。
这是 React 的一个缺点,默认状况下简直没有任何性能优化。
咱们来看一下动图演示:
购物车此时有 5 个商品,看控制台的打印,每次都是以 5 为倍数增长每点击一次 checkbox,都会触发所有子组件的从新渲染。
如果咱们有 50 个商品在购物车中,咱们改了其中某一项的checked
状态,也会导致 50 个子组件从新渲染。
咱们想到了一个 api: React.memo
,这个 api 根本等效于 class 组件中的shouldComponentUpdate
,如果咱们用这个 api 让子组件只有在 checked 产生扭转的时候再从新渲染呢?
好,咱们进入子组件的编写:
// memo优化策略function areEqual(prevProps: Props, nextProps: Props) { return prevProps.checked === nextProps.checked}const ItemCard: FC<Props> = React.memo((props) => { const { checked, onCheckedChange } = props return ( <div> <checkbox value={checked} onChange={(value) => onCheckedChange(cartItem, value)} /> <span>商品</span> </div> )}, areEqual)
在这种优化策略下,咱们认为只有前后两次渲染传入的 props 中的checked
相等,那么就不去从新渲染子组件。
React Hook 的古老值导致的 bug
到这里就实现了吗?其实,这里是有 bug 的。
咱们来看一下 bug 还原:
如果咱们先点击了第一个商品的勾选,再点击第二个商品的勾选,你会发现第一个商品的勾选状态没了。
在勾选了第一个商品后,咱们此时的最新的checkedMap
其实是
{ 1: true }
而因为咱们的优化策略,第二个商品在第一个商品勾选后没有从新渲染,
留神 React 的函数式组件,在每次渲染的时候都会从新执行,从而产生一个闭包环境。
所以第二个商品拿到的onCheckedChange
还是前一次渲染购物车这个组件的函数闭包中的,那么checkedMap
天然也是上一次函数闭包中的最后的空对象。
const onCheckedChange: OnCheckedChange = (cartItem, checked) => { const { id } = cartItem // 留神,这里的checkedMap还是最后的空对象!! const newCheckedMap = Object.assign({}, checkedMap, { [id]: checked, }) setCheckedMap(newCheckedMap)}
因而,第二个商品勾选后,没有依照预期的计算出正确的checkedMap
{ 1: true, 2: true}
而是计算出了谬误的
{ 2: true }
这就导致了第一个商品的勾选状态被丢掉了。
这也是 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} /> )
子组件
// memo优化策略function areEqual(prevProps: Props, nextProps: Props) { return prevProps.checked === nextProps.checked}const ItemCard: FC<Props> = React.memo((props) => { const { checked, onCheckedChangeRef } = props return ( <div> <checkbox value={checked} onChange={(value) => onCheckedChangeRef.current(cartItem, value)} /> <span>商品</span> </div> )}, areEqual)
到此时,咱们的简略的性能优化就实现了。
自定义 hook 之 useChecked
那么下一个场景,又遇到这种全选反选相似的需要,难道咱们再这样反复写一套吗?这是不可承受的,咱们用自定义 hook 来形象这些数据以及行为。
并且这次咱们通过 useReducer 来防止闭包旧值的陷阱(dispatch 在组件的生命周期中放弃惟一援用,并且总是能操作到最新的值)。
import { useReducer, useEffect, useCallback } from 'react'interface Option { /** 用来在map中记录勾选状态的key 个别取id */ key?: string}type CheckedMap = { [key: string]: boolean}const CHECKED_CHANGE = 'CHECKED_CHANGE'const CHECKED_ALL_CHANGE = 'CHECKED_ALL_CHANGE'const SET_CHECKED_MAP = 'SET_CHECKED_MAP'type CheckedChange<T> = { type: typeof CHECKED_CHANGE payload: { dataItem: T checked: boolean }}type CheckedAllChange = { type: typeof CHECKED_ALL_CHANGE payload: boolean}type SetCheckedMap = { type: typeof SET_CHECKED_MAP payload: CheckedMap}type Action<T> = CheckedChange<T> | CheckedAllChange | SetCheckedMapexport type OnCheckedChange<T> = (item: T, checked: boolean) => any/** * 提供勾选、全选、反选等性能 * 提供筛选勾选中的数据的函数 * 在数据更新的时候主动剔除古老项 */export const useChecked = <T extends Record<string, any>>( dataSource: T[], { key = 'id' }: Option = {}) => { const [checkedMap, dispatch] = useReducer( (checkedMapParam: CheckedMap, action: Action<T>) => { switch (action.type) { case CHECKED_CHANGE: { const { payload } = action const { dataItem, checked } = payload const { [key]: id } = dataItem return { ...checkedMapParam, [id]: checked, } } case CHECKED_ALL_CHANGE: { const { payload: newCheckedAll } = action const newCheckedMap: CheckedMap = {} // 全选 if (newCheckedAll) { dataSource.forEach((dataItem) => { newCheckedMap[dataItem.id] = true }) } return newCheckedMap } case SET_CHECKED_MAP: { return action.payload } default: return checkedMapParam } }, {} ) /** 勾选状态变更 */ const onCheckedChange: OnCheckedChange<T> = useCallback( (dataItem, checked) => { dispatch({ type: CHECKED_CHANGE, payload: { dataItem, checked, }, }) }, [] ) type FilterCheckedFunc = (item: T) => boolean /** 筛选出勾选项 能够传入filter函数持续筛选 */ const filterChecked = useCallback( (func: FilterCheckedFunc = () => true) => { return ( Object.entries(checkedMap) .filter((entries) => Boolean(entries[1])) .map(([checkedId]) => dataSource.find(({ [key]: id }) => id === Number(checkedId)) ) // 有可能勾选了当前间接删除 此时id尽管在checkedMap里 然而dataSource里曾经没有这个数据了 // 先把空项过滤掉 保障内部传入的func拿到的不为undefined .filter(Boolean) .filter(func) ) }, [checkedMap, dataSource, key] ) /** 是否全选状态 */ const checkedAll = dataSource.length !== 0 && filterChecked().length === dataSource.length /** 全选反选函数 */ const onCheckedAllChange = (newCheckedAll: boolean) => { dispatch({ type: CHECKED_ALL_CHANGE, payload: newCheckedAll, }) } // 数据更新的时候 如果勾选中的数据曾经不在数据内了 就删除掉 useEffect(() => { filterChecked().forEach((checkedItem) => { let changed = false if (!dataSource.find((dataItem) => checkedItem.id === dataItem.id)) { delete checkedMap[checkedItem.id] changed = true } if (changed) { dispatch({ type: SET_CHECKED_MAP, payload: Object.assign({}, checkedMap), }) } }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataSource]) return { checkedMap, dispatch, onCheckedChange, filterChecked, onCheckedAllChange, checkedAll, }}
这时候在组件内应用,就很简略了:
const { checkedAll, checkedMap, onCheckedAllChange, onCheckedChange, filterChecked,} = useChecked(cartData)
咱们在自定义 hook 里把简单的业务逻辑全副做掉了,包含数据更新后的有效 id 剔除等等。快去推广给团队的小伙伴,让他们早点上班吧。
自定义 hook 之 useMap
有一天,忽然又来了个需要,咱们须要用一个 map 来依据购物车商品的 id 来记录另外的一些货色,咱们忽然发现,下面的自定义 hook 把 map 的解决等等逻辑也都打包进去了,咱们只能给 map 的值设为true / false
,灵活性不够。
咱们进一步把useMap
也抽出来,而后让useCheckedMap
基于它之上开发。
useMap
import { useReducer, useEffect, useCallback } from 'react'export interface Option { /** 用来在map中作为key 个别取id */ key?: string}export type MapType = { [key: string]: any}export const CHANGE = 'CHANGE'export const CHANGE_ALL = 'CHANGE_ALL'export const SET_MAP = 'SET_MAP'export type Change<T> = { type: typeof CHANGE payload: { dataItem: T value: any }}export type ChangeAll = { type: typeof CHANGE_ALL payload: any}export type SetCheckedMap = { type: typeof SET_MAP payload: MapType}export type Action<T> = Change<T> | ChangeAll | SetCheckedMapexport type OnValueChange<T> = (item: T, value: any) => any/** * 提供map操作的性能 * 在数据更新的时候主动剔除古老项 */export const useMap = <T extends Record<string, any>>( dataSource: T[], { key = 'id' }: Option = {}) => { const [map, dispatch] = useReducer( (checkedMapParam: MapType, action: Action<T>) => { switch (action.type) { // 单值扭转 case CHANGE: { const { payload } = action const { dataItem, value } = payload const { [key]: id } = dataItem return { ...checkedMapParam, [id]: value, } } // 所有值扭转 case CHANGE_ALL: { const { payload } = action const newMap: MapType = {} dataSource.forEach((dataItem) => { newMap[dataItem[key]] = payload }) return newMap } // 齐全替换map case SET_MAP: { return action.payload } default: return checkedMapParam } }, {} ) /** map某项的值变更 */ const onMapValueChange: OnValueChange<T> = useCallback((dataItem, value) => { dispatch({ type: CHANGE, payload: { dataItem, value, }, }) }, []) // 数据更新的时候 如果map中的数据曾经不在dataSource内了 就删除掉 useEffect(() => { dataSource.forEach((checkedItem) => { let changed = false if ( // map中蕴含此项 // 并且数据源中找不到此项了 checkedItem[key] in map && !dataSource.find((dataItem) => checkedItem[key] === dataItem[key]) ) { delete map[checkedItem[key]] changed = true } if (changed) { dispatch({ type: SET_MAP, payload: Object.assign({}, map), }) } }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataSource]) return { map, dispatch, onMapValueChange, }}
这是一个通用的 map 操作的自定义 hook,它思考了闭包陷阱,思考了旧值的删除。
在此之上,咱们实现下面的useChecked
useChecked
import { useCallback } from 'react'import { useMap, CHANGE_ALL, Option } from './use-map'type CheckedMap = { [key: string]: boolean;}export type OnCheckedChange<T> = (item: T, checked: boolean) => any/** * 提供勾选、全选、反选等性能 * 提供筛选勾选中的数据的函数 * 在数据更新的时候主动剔除古老项 */export const useChecked = <T extends Record<string, any>>( dataSource: T[], option: Option = {}) => { const { map: checkedMap, onMapValueChange, dispatch } = useMap( dataSource, option ) const { key = 'id' } = option /** 勾选状态变更 */ const onCheckedChange: OnCheckedChange<T> = useCallback( (dataItem, checked) => { onMapValueChange(dataItem, checked) }, [onMapValueChange] ) type FilterCheckedFunc = (item: T) => boolean /** 筛选出勾选项 能够传入filter函数持续筛选 */ const filterChecked = useCallback( (func?: FilterCheckedFunc) => { const checkedDataSource = dataSource.filter(item => Boolean(checkedMap[item[key]]) ) return func ? checkedDataSource.filter(func) : checkedDataSource }, [checkedMap, dataSource, key] ) /** 是否全选状态 */ const checkedAll = dataSource.length !== 0 && filterChecked().length === dataSource.length /** 全选反选函数 */ const onCheckedAllChange = (newCheckedAll: boolean) => { // 全选 const payload = !!newCheckedAll dispatch({ type: CHANGE_ALL, payload, }) } return { checkedMap: checkedMap as CheckedMap, dispatch, onCheckedChange, filterChecked, onCheckedAllChange, checkedAll, }}
总结
本文通过一个实在的购物车需要,一步一步的实现优化、踩坑,在这个过程中,咱们对 React Hook 的优缺点肯定也有了进一步的意识。
在利用自定义 hook 把通用逻辑抽取进去后,咱们业务组件内的代码量大大的缩小了,并且其余类似的场景都能够去复用。
React Hook 带来了一种新的开发模式,然而也带来了一些陷阱,它是一把双刃剑,如果你能正当应用,那么它会给你带来很弱小的力量。
感激你的浏览,心愿这篇文章能够给你启发。