Angular實現虛擬滾動多選下拉框筆記

zzszzs發表於2019-03-16

要求:

實現一個angular多選下拉框元件,當有超過2000個選項時,滑動/挑選/全選均不卡。

正篇:

為了方便,這裡不考慮擴充套件性,所以規定下拉框展開只顯示7行資料,行高27px

Angular實現虛擬滾動多選下拉框筆記

約定: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多選下拉框。


相關文章