对于已经开发好的长列表,如何做简易版性能优化
- 使用react-window改造渲染省市区的长列表
- 简易版性能优化方法
使用react-window改造渲染省市区的长列表
- 不使用虚拟滚动渲染省市区列表
- 使用虚拟滚动改造
- 虚拟滚动参数分析
- 处理数据结构,修改渲染逻辑
不使用虚拟滚动渲染省市区列表
首先确定数据结构,这里只列出一个省市区
[ { "label": "北京市", "value": "北京市", "level": "one", children: [ "label": "北京市", "value": "北京市", "level": "two" "children": [{ "label": "东城区", "value": "东城区", "level": "three" }]
]
}
]
这里需要用到递归,有children的话我们就继续渲染
import React from 'react'
import areaList from './province.json'
import './index.css'
const Page = () => {
const renderArea = (list) => {
return list.map(item => {
return (
<div key={item.label} className={item.level}>
{ item.label }
{ item.children && renderArea(item.children) }
</div>
)
})
}
return (
<div className="wrapper">
{ renderArea(areaList) }
</div>
)
}
export default Page
效果:
使用虚拟滚动改造
先看下虚拟滚动组件需要的参数
const Row = ({ index, style }) => {
return (
<div style={style}></div>
)
}
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
width={width} //列表可视区域的宽度
height={height} // 列表可视区域的高度
itemCount={1} // 列表数据长度
itemSize={40} // 列表行高
>
{ Row }
</FixedSizeList>
)}
</AutoSizer>
虚拟滚动参数分析
itemCount:虚拟滚动项的数量,在这里就是省市区的数量。 Row组件:每一个虚拟滚动项要渲染的内容。Row会接收参数index,我们可以通过index知道正在渲染哪一项,再通过省市区List[index]来得到要渲染的内容。 目标:得到一个省市区List,这个List是一个一维数组,形如下面的结构才能满足我们的需求。
[ { "label": "北京市", "value": "北京市", level: "one" }, { "label": "北京市", "value": "北京市", level: "two" }, { "label": "东城区", "value": "东城区", level: "three" },]
处理数据结构,修改渲染逻辑
遍历树形省市区列表,转为一维数组
const getFlatList = (originList, target) => {
originList.forEach(item => {
const { children, ...rest } = item
target.push({
...rest
})
if (children) {
getFlatList(children, target)
}
})
}
const list = []
getFlatList(provinceList, list)
console.log('list', JSON.stringify(list))
得到一维数组后可以放到本地json文件,得到一维数组后,根据Row组件给的index参数就可以知道当前应该渲染的内容了。
const Row = ({ index, style }) => {
const currentItem = provinceList[index]
return (
<div style={style} className={currentItem.level}>
{currentItem.label}
</div>
)
}
<div style={{ height: '400px'}} className="wrapper">
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
width={width} //列表可视区域的宽度
height={height} // 列表可视区域的高度
itemCount={provinceList.length} // 列表数据长度
itemSize={40} // 列表行高
>
{ Row }
</FixedSizeList>
)}
</AutoSizer>
</div>
效果:
现在使用react-window改造完成了,可以看到,我们原本写的递归遍历渲染地区的方法已经不需要了,取而代之的是一个接收index来渲染内容的方法,同时我们需要处理数据结构,树形结构的数据要扁平化为一维数组。总体看下来就是要处理数据结构,并且修改渲染逻辑,如果在业务复杂的情况修改渲染逻辑那成本就很大了。
简易版性能优化方法
简易版性能优化的方法,思路就是首次渲染时只渲染前面几十项,等滚动条移动到一定高度时,再往后渲染几十项。这样也可以避免首次加载过多的dom节点导致的页面卡顿。和虚拟滚动不同的是,当滚动条移动到下方时,我们并不会清掉之前渲染好的dom节点,但经测试渲染完后的页面即使页面节点多,在我们拖动滚动条时也并不会很卡,页面卡死只在首屏渲染时创建节点过多时发生,所以我们可以使用这种简易版性能优化方法。
- 减少首屏渲染数据
- 监听滚动条,到达一定高度时触发下次渲染
减少首屏渲染数据
我们先筛选出3个地区的数据,3个地区对比渲染所有地区已经能减少一定数量的dom节点,而且能保证出现滚动条,如果只渲染1个地区可能由于数据量不够而不出现滚动条,所以我们选择渲染3个地区。
import originList from './province.json'
const [areaList, setAreaList] = useState(originList.slice(0, 3))
const renderArea = (list) => {
return list.map(item => {
return (
<div key={item.label} className={item.level}>
{ item.label }
{ item.children && renderArea(item.children) }
</div>
)
})
}
<div className="wrapper">
{ renderArea(areaList) }
</div>
效果:
监听滚动条,到达一定高度时触发下次渲染
当满足scrollTop + clientHeight >= documentHeight时代表滚动到底部,我们可以让滚动条滚动到文档总高度80%时就往数组再push一个地区,push之前可以先判断一下是否存在下一个地区。
const [areaList, setAreaList] = useState(originList.slice(0, 3))
const curAreaIdx = useRef(3)
useEffect(() => {
window.addEventListener('scroll', function() {
const clientHeight = document.documentElement.clientHeight
const scrollTop = document.documentElement.scrollTop
const documentHeight = document.documentElement.scrollHeight
if (
(scrollTop + clientHeight >= documentHeight * 0.8) && (originList[curAreaIdx.current])
) {
console.log('滚动到总高度的80%')
const list = [...areaList, originList[curAreaIdx.current]]
setAreaList(list)
curAreaIdx.current += 1
}
})
}, [])
其中clientHeight、scrollTop、scrollHeight涉及兼容性的写法,这里就直接使用documentElement来获取了。curAreaIdx用来记录当前应该渲染哪一个地区,渲染结束后也要去更新这个变量。
效果:
可以看到,第一次推入一个地区后,数组长度为4,再一次推入一个地区时数组长度仍然为4。这是因为const list = [...areaList, originList[curAreaIdx.current]]这句代码里我们是在useEffect里去获取areaList,而areaList永远是我们初始化时的三个地区的数组
useEffect里获取到旧的state的解决办法
即使我们已经重新渲染了视图,areaList的值已经变化,useEffect也获取不到最新的areaList,这里与函数组件依赖于闭包有关,在useEffect获取areaList获取到的是某次特定渲染的值。我们使用的是useEffect(() => {}, []),则areaList的值永远是挂载时的areaList的值。要解决这个问题可以使用useRef来保存已经渲染好的数组,因为useRef的值不受限于某次特定的渲染。
const [areaList, setAreaList] = useState(originList.slice(0, 3))
const curAreaList= useRef(originList.slice(0, 3))
useEffect(() => {
window.addEventListener('scroll', function() {
const clientHeight = document.documentElement.clientHeight
const scrollTop = document.documentElement.scrollTop
const documentHeight = document.documentElement.scrollHeight
if (
(scrollTop + clientHeight >= documentHeight * 0.8) && (originList[curAreaList.current.length])
) {
console.log('滚动到总高度的80%')
const list = [...curAreaList.current, originList[curAreaList.current.length]]
setAreaList(list)
console.log('list', list)
curAreaList.current = list
}
})
}, [])
我们使用curAreaList来保存渲染好的数组,每次推入新的地区后,再更新下curAreaList的值
效果:
这样就完成了简易版的性能优化,不需要改数据结构,也不需要改原来的渲染逻辑,对比使用react-window开发成本更低。
转载自:https://juejin.cn/post/7373226679731142666