Angular实现虚拟滚动多选下拉框笔记

2,013 阅读3分钟

要求:

实现一个angular多选下拉框组件,当有超过2000个选项时,滑动/挑选/全选均不卡。

正篇:

为了方便,这里不考虑扩展性,所以规定下拉框展开只显示7行数据,行高27px

约定:input为一个叫做list的SelectItem[],SelectItem大概定义如下:

export class SelectItem {    
    public label: string;    
    public value: string;    
    public disabled?: boolean; // 某些选项禁用    
    public groupKey?: string; // 给选项分组用    
    public groupHead?: string; // 显示选项组    
    public checked?: boolean; // 是否打勾    
    public tempChecked?: boolean; // 是否临时打勾(用户要求,自动保存选项的话不需要)    
    public originalCheckStatus?: boolean; // 放弃保存(用户要求,自动保存选项的话不需要)
}

每次我们给用户看到的选项只有6个,所以当整个数组进来时我们最少只用截取6个;又因为规定每条高度27px,所以包含选项的框162px

this.actualList = this.list.slice(0, 8); // 多截两个当缓冲

.list-container {height: 162px;}

所以在html画出来时用户就刚好看见6个 (trackBy是额外的优化)

<div class="list-container">
    <div *ngFor="let item of actualList; trackBy: trackByValue" class="inner-option">
        <label>
            <input type="checkbox" [(ngModel)]="item.checked">
            <span>{{item.label}}</span>
        </label>
    </div>
<div>

可这个时候滑动条是满的不能滑动,我们需要给这个面板一个假的高度把面板撑开

<div class="list-container">
    <div [style.height]="scrollHeight">
        <div *ngFor="let item of actualList; trackBy: trackByValue" class="inner-option">
            <label>
                <input type="checkbox" [(ngModel)]="item.checked">
                <span>{{item.label}}</span>
            </label>
        </div>
    </div>
</div>

this.scrollHeight = this.list.length * 27;

.list-container {
    height: 162px;
    overflow: auto; // 当然你要让这个外层溢出后加滚动
}

这样你就得到了一个看起来可以滚动的但是其实只有第一页的下拉框。然后你要做的就是在用户滚动之后动态的更新actualList,这里我们可以用angular封装好的scroll event:

<div class="list-container" (scroll)="onScroll($event)">
    <div [style.height]="scrollHeight">
        <div *ngFor="let item of actualList; trackBy: trackByValue" class="inner-option">
            <label>
                <input type="checkbox" [(ngModel)]="item.checked">
                <span>{{item.label}}</span>
            </label>
        </div>
    </div>
</div>

public onScroll(e) {
    const firstIndex = Math.floor(e.target.scrollTop / 27);
    const secondIndex = firstIndex + 8;
    this.actualList = this.list.slice(firstIndex, secondIndex);
}

以上虽然你是更新了可以显示的实际list,但是还没有在面板上显示出来,因为这个actualList被你划上去了,所以接下来需要在滚动时动态的把这一块往下/上移,这里我们用translateY

public onScroll(e) {
...
this.translateY = `translateY(${firstIndex * 27}px)`;
}

<div>
...
    <div *ngFor=....class="inner-option" [style.transform]="translateY">
...
</div>

其实到这里核心的整个虚拟滚动的列表就制作完成了,剩下的工作包括:

  1. 并不是每一点滚动都需要重新计算actualList的,只有最上面的那个滚出去了之后才需要重新render
  2. 这里每次重新得到actualList之后浏览器要移除并添加节点在dom上,最优的情况应该是重用这些节点,让第一个和最后一个节点来回移动
  3. check all怎么跟下面的子checkbox同步起来,每个group的checkbox怎么跟其他checkbox同步起来
  4. 下拉框的按钮怎么显示用户选好的项目
  5. 怎么放弃选择,点ok保存
  6. 怎么区分用户正在点下拉框外面(关下拉框)还是里面正常操作
  7. 怎么实时监听输入的list的变化,用户的选择变化

下篇再说

---------------------------------其他-----------------------------

背景:

目前市面上的很多下拉框(包括bootstrap的各种angular/react实现,kendoUI,antd)都还没有考虑大数据量的情况。这样的话一旦数据突破4000条(单选)/1000(多选),浏览器就会变卡,因为DOM tree已经绘制了所有这些nodes。

吐槽:

这种情况很少见,但不是没有,比如我们的场景是允许用户创建自己的条目,一旦共享之后这条项目就会出现在某个多选框里让其他人自由组合使用,数据量就很容易变大。

发现:

Angular更新7的时候我注意到他们的material design库新增加了一个新玩意:虚拟滚动。本质上就是只绘制用户看到的节点去极大地节省内存开销。blog.angular.io/version-7-o…

过了不久后看见了阿健大叔在前端之巅发表的《如何用react+rxjs实现一个虚拟滚动组件》受启发,就决定用这种思想重写我们正在用的angular多选下拉框。