懒渲染背后有多细节?

懒渲染背后有多细节?

十月 25, 2021 本文共计: 1.6k 字 预计阅读时长: 6分钟

这片文章可能略微有些标题狗了,不过,大致差不多~

引子

最近在做一个东西,因为后端同学比较忙的缘故+跟他关系好,所以答应了对于某个接口的数据的查询做全量数据返回,前端做滚动懒渲染+模糊查询。

听起来可能比较高大上,但是实际上想明白了就很简单了。

正题

首先说明下,这个数据量有多大?预计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这个方法其实还是值得推敲的,因为每次一调用就会触发分割。那么我们在调试时候的打印,也会触发分割,其实还是不希望的。

所以,这里做了一个时间为10ms的数据分割阈值控制,做一个阀门控制住分割的并发数,在并发的时间内,返回同一个chunk单例。

最后,销毁重置所有数据态

/**
 * @desc 及时销毁内存,避免浪费或者内存溢出
 */
destroy() {
    this._dataList = [];
    this._WIPDataList = [];
    this._chunkDataList = [];
    this._recordGetChunkCount = 0;
    this._isWIP = false;
    this._actionTime = Date.now();
}

演示

说了那么多,其实我们并没有看到实际的效果,可以大致观摩下~

模糊搜索

怎么样,还不错吧,这个效果,对于我们的数据加载,甚至性能都有很大的优化~