一、需要解决的问题
1.首次加载缓慢;
2.修改表单时,有时会修改失败。
二、问题分析
1.使用react-scan插件查看加载慢的问题,发现首次进入页面的时候会页面全屏重复加载多次的问题,每次修改表单内容都会触发整个应用的重新渲染。
2.重复的React.useEffect订阅,有多个useEffect同时订阅store的变化。
3.更新函数触发连锁反应,每当表单项发生变化时, `updateItem` 会更新areas,这触发了areas订阅,由于订阅的store是绑定的全局app.store所以会造成全局渲染。
三、问题解决
1. 优化订阅逻辑,减少不必要的更新触发
// 修改前
React.useEffect(
() =>
store.subscribe(
(s) => s.areas,
(areas) => {
if (!task || step !== 1) return
console.log('areas changed')
updateTaskThrottledCallback({ ...task, areas })
}
),
[task, step]
)
React.useEffect(
() =>
store.subscribe(
(s) => s.roiTransformer,
(roiTransformer) => {
if (!task || step !== 1) return
console.log('roiTransformer changed')
updateTaskThrottledCallback({ ...task, roiTransformer })
}
),
[task, step]
)
React.useEffect(
() =>
store.subscribe(
(s) => s.imagePreprocessing,
(imagePreprocessing) => {
if (!task || step !== 1) return
console.log('imagePreprocessing changed')
updateTaskThrottledCallback({ ...task, imagePreprocessing })
}
),
[task, step]
)
React.useEffect(() => {
if (!device) return
setConfiguringStep(device.id, step)
setTaskName(task?.name ?? '')
}, [step])
// 修改后
React.useEffect(
() =>
store.subscribe(
(s) => ({ areas: s.areas, roiTransformer: s.roiTransformer, imagePreprocessing: s.imagePreprocessing }),
({ areas, roiTransformer, imagePreprocessing }) => {
if (!task || step !== 1) return
// 添加深度比较,避免因为对象引用变化而触发不必要的更新
const hasRealChanges =
JSON.stringify(task.areas) !== JSON.stringify(areas) ||
JSON.stringify(task.roiTransformer) !== JSON.stringify(roiTransformer) ||
JSON.stringify(task.imagePreprocessing) !== JSON.stringify(imagePreprocessing)
if (!hasRealChanges) return
console.log('task data changed')
updateTaskThrottledCallback({ ...task, areas, roiTransformer, imagePreprocessing })
},
{
equalityFn: (a, b) =>
a.areas === b.areas &&
a.roiTransformer === b.roiTransformer &&
a.imagePreprocessing === b.imagePreprocessing
}
),
[task, step]
)
2. 拆解store
使用一个对应模块的store去拆解全局的app.store或者新建一个临时的store,在合适的时机(如点击完成按钮),去更新到全局的store.
3. 表单组件的优化
使用 useCallback 包裹了 handleItemChecked 、 handleItemChanged 和 handleFormatChange 等更新方法.
useCallback
是一个允许你在多次渲染中缓存函数的 React Hook。useCallback – React 中文文档
memo
允许你的组件在 props 没有改变的情况下跳过重新渲染。memo – React 中文文档
使用Memo包裹子组件
const Item = React.memo(({ item, itemIndex, handleItemChanged, handleFormatChange }) => {
// 组件逻辑
})
4. 事件处理函数优化
// 优化前
const handleItemChecked = (itemIndex: number, checked: boolean) => {
// 直接更新逻辑
}
// 优化后
const handleItemChecked = useCallback((itemIndex: number, checked: boolean) => {
const item = tool.config.form.items[itemIndex]
if (item.enabled === checked) return // 避免重复更新
const updatedItem = { ...item, enabled: checked }
tempFormStore.getState().addPendingUpdate(areaIndex, toolIndex, itemIndex, updatedItem)
}, [tool.config.form.items, areaIndex, toolIndex])
5. 订阅逻辑优化
缺乏深度比较,对象引用变化就会触发更新
没有检查数据是否真正发生变化
// 优化前
React.useEffect(
() =>
store.subscribe(
(s) => ({ areas: s.areas, roiTransformer: s.roiTransformer, imagePreprocessing: s.imagePreprocessing }),
({ areas, roiTransformer, imagePreprocessing }) => {
if (!task || step !== 1) return
console.log('task data changed')
updateTaskThrottledCallback({ ...task, areas, roiTransformer, imagePreprocessing })
}
),
[task, step]
)
// 优化后
React.useEffect(
() =>
store.subscribe(
(s) => ({ areas: s.areas, roiTransformer: s.roiTransformer, imagePreprocessing: s.imagePreprocessing }),
({ areas, roiTransformer, imagePreprocessing }) => {
if (!task || step !== 1) return
// 添加深度比较,避免因为对象引用变化而触发不必要的更新
const hasRealChanges =
JSON.stringify(task.areas) !== JSON.stringify(areas) ||
JSON.stringify(task.roiTransformer) !== JSON.stringify(roiTransformer) ||
JSON.stringify(task.imagePreprocessing) !== JSON.stringify(imagePreprocessing)
if (!hasRealChanges) return
console.log('task data changed')
updateTaskThrottledCallback({ ...task, areas, roiTransformer, imagePreprocessing })
}
),
[task, step]
)
6. 区域组件渲染优化
乏精确的比较函数,自定义比较函数,只在真正需要更新时才重新渲染
const AreaItem = React.memo(({ areaIndex, area, onShowPanel }) => {
// 组件逻辑
}, (prevProps, nextProps) => {
// 自定义比较函数,只在真正需要更新时才重新渲染
return (
prevProps.areaIndex === nextProps.areaIndex &&
prevProps.area.tools.length === nextProps.area.tools.length &&
prevProps.area.points.length === nextProps.area.points.length &&
JSON.stringify(prevProps.area.tools) === JSON.stringify(nextProps.area.tools) &&
JSON.stringify(prevProps.area.points) === JSON.stringify(nextProps.area.points)
)
})