长列表中每一行如果包含表单元素或者图片等复杂组件时,修改某一行文本框,会发现非常卡顿。
如下示例代码,长列表有 500 行,修改第一行文本框,会发现每次即便修改一个字符,所有行都会重新渲染一遍 (控制台会打印 500 次 “Row”),这是导致卡顿的根本原因。
import React, { useState } from 'react'
// 构造 500 条数据 Array<{ id: number, name: string }>
const data = Array.from({ length: 500 }, (_, index) => ({ id: index + 1, name: String(Math.random()) }))
const Table = () => {
const [ dataSource, setDataSource ] = useState(data)
const onChange = (id, value) => {
setDataSource(dataSource.map(d => {
if (d.id === id) {
d.name = value
}
return d
}))
}
return (
<div>
{
dataSource.map(rowData => {
return (
<Row
key={rowData.id}
rowData={rowData}
onChange={onChange}
/>
)
})
}
</div>
)
}
const Row = ({
rowData,
onChange,
}: {
rowData: { id: number; name: string; };
onChange: (id: number, value: string) => void;
}) => {
console.log('Row');
return (
<div>
<span>{rowData.id}</span>
<input
value={rowData.name}
onChange={e => onChange?.(rowData.id, e.target.value)}
/>
</div>
)
}
export default TestPage
我们期待的结果是: 只操作某一行内的文本框时,其它行不要进行渲染。
给 Row 组件套上 memo(),此时只要 rowData 和 onChange 不变化,就不会重新渲染的。
比如修改第一行文本框数据,其它行的 rowData 是不会变化的,这非常容易理解。
但是 onChange 是个函数,每次修改第一行文本框数据时,onChange 这个函数的函数引用会重新生成。
const Row = memo(({
rowData,
onChange,
}: any) => {
console.log('Row');
return (
<div>
<span>{rowData.id}</span>
<input
value={rowData.name}
onChange={e => onChange?.(e.target.value)}
/>
</div>
)
})
修改 Table 组件,用 useCallback 钩子包裹 onChange 函数:
const Table = () => {
const [ dataSource, setDataSource ] = useState(data)
const onChange = useCallback((id, value) => {
setDataSource(dataSource.map(d => {
if (d.id === id) {
d.name = value
}
return d
}))
}, [])
return (
<div>
{
dataSource.map(rowData => {
return (
<Row
key={rowData.id}
rowData={rowData}
onChange={onChange}
/>
)
})
}
</div>
)
}
理论上这么改是可以保证 onChange 函数引用不会改变,测试会发现这会儿压根没法修改文本框内的值。
原因是 useCallback 内部构成了闭包,在 useCallback 内部获取的 dataSource 永远是第一次页面渲染时的变量值。
要解决这个问题,我们只需要在 useCallback 内部改变写法即可。
const onChange = useCallback((id, value) => {
// 旧写法 setDataSource(dataSource)
// setDataSource(dataSource.map(d => {
// if (d.id === id) {
// d.name = value
// }
// return d
// }))
// 新写法:回调函数方式 setDataSource(prev => prev)
setDataSource(prev => prev.map(p =>
p.id === id ? { ...p, name: value } : p,
))
}, [ ])
完整示例如下:
import React, { memo, useCallback, useState } from 'react'
const data = Array.from({ length: 500 }, (_, index) => ({ id: index + 1, name: String(Math.random()) }))
const Table = () => {
const [ dataSource, setDataSource ] = useState(data)
const onChange = useCallback((id, value) => {
setDataSource(prev => {
return prev.map(p => {
if (p.id === id) {
return { ...p, name: value }
}
return p
})
})
}, [])
return (
<div>
{
dataSource.map(rowData => {
return (
<Row
key={rowData.id}
rowData={rowData}
onChange={onChange}
/>
)
})
}
</div>
)
}
const Row = memo(({
rowData,
onChange,
}: {
rowData: { id: number; name: string; };
onChange: (id: number, value: string) => void;
}) => {
console.log('Row');
return (
<div>
<span>{rowData.id}</span>
<input
value={rowData.name}
onChange={e => onChange?.(rowData.id, e.target.value)}
/>
</div>
)
})
export default Table
当然了,对于长列表也可以做成虚拟列表的方式来优化性能,动态加载视口内的元素,参考 《reactjs react-intersection-observer 实现虚拟列表》。
↶ 返回首页 ↶