懒渲染背后有多细节?
这片文章可能略微有些标题狗了,不过,大致差不多~
引子
最近在做一个东西,因为后端同学比较忙的缘故+
跟他关系好,所以答应了对于某个接口的数据的查询做全量数据返回,前端做滚动懒渲染+
模糊查询。
听起来可能比较高大上,但是实际上想明白了就很简单了。
正题
首先说明下,这个数据量有多大?预计8000+
左右,也就是如果不做懒渲染的话,前端界面的某一个部分一次性要渲染8000+
个DOM
。
1. 这一定会很卡的!(至少我听到后的第一反应是这个)
2. 没有一个正常人会在8000+
条数据中找某一条数据的,眼睛都看花了,但是不排除有,大概定级为使用场景出现频率低
How to do it?
怎么做呢?我们来理一下思路:
正常渲染
- 收集数据
- 切割数据
- 滚动渲染
模糊搜索
- 模糊搜索
- 切割数据
- 滚动渲染
思路理清楚了,我们做一下代码展示和代码分析吧~
代码演示
import { filter } from 'lodash-es';
type DataShape = Array<
{
id: number;
name: string;
} & Record<string, unknown>
>;
class DataEngine {
// 分割 chunk 片的阈值
static SPLIT_CHUNK_UNIT = 40;
// 并发时间范围的阈值, 10 毫秒
static CONCURRENT_TIME_DELAY = 10;
// 初始化数据,一般指代的是全量数据
private _dataList: DataShape;
// 分片_chunkData数据
private _chunkDataList: DataShape[];
// 记录分割的 chunk 数目
private _recordGetChunkCount: number;
// 工作区数据(常规状态 + 模糊搜索态)
private _WIPDataList: DataShape;
// 是否处于模糊搜索态
private _isWIP: boolean;
// 时间阈值
private _actionTime: number | null;
// 单例 chunkData,应付在一定时间阈值内的并发调用获取
private _singleChunkDataList: DataShape;
constructor() {
this._dataList = [];
this._WIPDataList = [];
this._chunkDataList = [];
this._recordGetChunkCount = 0;
this._isWIP = false;
this._actionTime = null;
this._singleChunkDataList = [];
}
setData(data: DataShape) {
this._dataList = data;
this._WIPDataList = data;
}
/**
* @desc 模糊搜索值,且不区分到底是哪个属性
*/
fuzzySearch(keyword: string) {
this._WIPDataList = filter(this._dataList, obj => {
const idList = String(obj.orgId)?.includes?.(keyword);
const nameList = obj.orgName?.toString()?.includes?.(keyword);
return idList || nameList;
});
}
/**
* @desc 数据 chunk 分割
*/
splitChunk() {
this._chunkDataList = [];
if (!this._isWIP) {
this._WIPDataList = this._dataList;
}
/** 重置 chunk 切割索引 */
this._recordGetChunkCount = 0;
for (let i = 0; i < Math.ceil(this._WIPDataList?.length / DataEngine.SPLIT_CHUNK_UNIT); i++) {
this._chunkDataList.push(
this._WIPDataList?.slice(i * DataEngine.SPLIT_CHUNK_UNIT, DataEngine.SPLIT_CHUNK_UNIT * (i + 1)),
);
}
}
/**
* @desc 获取分片数据
*/
getChunk(): DataShape {
// 优先判断有没有设置时间值(其实就是第一次获取),其次判断时间阈值的范围
if (!this._actionTime || Date.now() - this._actionTime > DataEngine.CONCURRENT_TIME_DELAY) {
this._singleChunkDataList = [];
if (this._chunkDataList.length >= 1) {
this._singleChunkDataList = this._chunkDataList?.[this._recordGetChunkCount] || [];
this._chunkDataList.unshift();
}
this._recordGetChunkCount += 1;
this._actionTime = Date.now();
}
return this._singleChunkDataList;
}
/**
* @desc 及时销毁内存,避免浪费或者内存溢出
*/
destroy() {
this._dataList = [];
this._WIPDataList = [];
this._chunkDataList = [];
this._recordGetChunkCount = 0;
this._isWIP = false;
this._actionTime = Date.now();
}
/**
* @desc 检测 chunk 片区是否为空
*/
get chunkIsEmpty() {
/** 最直观的方式是检测,它的记录长度是否等于分割长度,分割长度取天花板 */
return this._recordGetChunkCount === Math.ceil(this._WIPDataList?.length / DataEngine.SPLIT_CHUNK_UNIT);
}
get cacheData() {
return this._WIPDataList;
}
get data() {
return this._dataList;
}
set WIP(flag: boolean) {
this._isWIP = flag;
}
}
这一大段可能看着有些累,没事,我们从头开始分析~
先解释下一些堆栈list
的含义:
// 分割 chunk 片的阈值
static SPLIT_CHUNK_UNIT = 40;
// 并发时间范围的阈值, 10 毫秒
static CONCURRENT_TIME_DELAY = 10;
// 初始化数据,一般指代的是全量数据
private _dataList: DataShape;
// 分片_chunkData数据
private _chunkDataList: DataShape[];
// 记录分割的 chunk 数目
private _recordGetChunkCount: number;
// 工作区数据(常规状态 + 模糊搜索态)
private _WIPDataList: DataShape;
// 是否处于模糊搜索态
private _isWIP: boolean;
// 时间阈值
private _actionTime: number | null;
// 单例 chunkData,应付在一定时间阈值内的并发调用获取
private _singleChunkDataList: DataShape;
首先是数据收集:
setData(data: DataShape) {
this._dataList = data;
this._WIPDataList = data;
}
这里的数据收集,主要是保存在_dataList
和_WIPDataList
中,其中_dataList
单纯是做全量数据缓存,而_WIPDataList
是做工作态的数据收集,其中初始态也属于工作态。
这里解释下工作态
- 初始化的状态
- 模糊搜索时的状态
- 销毁时的状态
其次是切割数据
/**
* @desc 数据 chunk 分割
*/
splitChunk() {
this._chunkDataList = [];
if (!this._isWIP) {
this._WIPDataList = this._dataList;
}
/** 重置 chunk 切割索引 */
this._recordGetChunkCount = 0;
for (let i = 0; i < Math.ceil(this._WIPDataList?.length / DataEngine.SPLIT_CHUNK_UNIT); i++) {
this._chunkDataList.push(
this._WIPDataList?.slice(i * DataEngine.SPLIT_CHUNK_UNIT, DataEngine.SPLIT_CHUNK_UNIT * (i + 1)),
);
}
}
每一次的数据切割都是对全量数据的操作,所以我们大可每次都初始化_chunkDataList
、_recordGetChunkCount
WIP
态的含义是:
- 全量数据态一定是
false
- 初始态全量
- 模糊搜索,搜索条件为空是全量
- 搜索条件不为空时,一定是
true
数据分割的chunk
范围是:i*DataEngine.SPLIT_CHUNK_UNIT
~ DataEngine.SPLIT_CHUNK_UNIT * (i + 1)
.这样可能不好理解,大白话叙述一下:
假如有90条数据,数据固定按照40条分割,那么可以分3块。
第一块:
0*40~40*1 === 0 ~ 40
(不包含40)第二块:
1*40~40*2 === 40 ~ 80
(不包含80)第三块:
2*40~40*3 === 80 ~ 120
(总共90条,一定可以拿全的)
模糊搜索
/**
* @desc 模糊搜索值,且不区分到底是哪个属性
*/
fuzzySearch(keyword: string) {
this._WIPDataList = filter(this._dataList, obj => {
const idList = String(obj.id)?.includes?.(keyword);
const nameList = obj.name?.toString()?.includes?.(keyword);
return idList || nameList;
});
}
获取chunk
/**
* @desc 获取分片数据
*/
getChunk(): DataShape {
// 优先判断有没有设置时间值(其实就是第一次获取),其次判断时间阈值的范围
if (!this._actionTime || Date.now() - this._actionTime > DataEngine.CONCURRENT_TIME_DELAY) {
this._singleChunkDataList = [];
if (this._chunkDataList.length >= 1) {
this._singleChunkDataList = this._chunkDataList?.[this._recordGetChunkCount] || [];
this._chunkDataList.unshift();
}
this._recordGetChunkCount += 1;
this._actionTime = Date.now();
}
return this._singleChunkDataList;
}
获取chunk
这个方法其实还是值得推敲的,因为每次一调用就会触发分割。那么我们在调试时候的打印,也会触发分割,其实还是不希望的。
所以,这里做了一个时间为10
ms的数据分割阈值控制,做一个阀门控制住分割的并发数,在并发的时间内,返回同一个chunk
单例。
最后,销毁重置所有数据态
/**
* @desc 及时销毁内存,避免浪费或者内存溢出
*/
destroy() {
this._dataList = [];
this._WIPDataList = [];
this._chunkDataList = [];
this._recordGetChunkCount = 0;
this._isWIP = false;
this._actionTime = Date.now();
}
演示
说了那么多,其实我们并没有看到实际的效果,可以大致观摩下~
模糊搜索
怎么样,还不错吧,这个效果,对于我们的数据加载,甚至性能都有很大的优化~