如何編寫一個高效能的Angular元件

釋迦摩尼發表於2018-03-04
這篇文章講述如何分析組建,編碼技巧,如何巧妙的規避一些巢狀迴圈,減少巢狀迴圈,低效能的編碼等。

案例

這次還是主要拿之前分享的一片文章中出現的業務組建,上次只是貼了程式碼,並沒有詳細說出實現過程,這次就以這個業務組建為中心,講述如何編寫一個高效能的業務元件。

如何編寫一個高效能的Angular元件


如何編寫一個高效能的Angular元件

根據上圖分析元件所要完成的功能

這個類似省市聯動的加強版,可以檢視被勾選的省市,並且核取方塊都有三個狀態,未選、全選、未全選,預設狀態只顯示根資料,點選相應的選項,如果有子集就會顯示對應的子集資料。

需求分解

這邊先將需求分解,一步一步的來實現功能

  1. 實現省市聯動。
  2. 實現全選功能,並且如果子集有未選項,父級狀態變更為未選全。
  3. 實現被勾選項以標籤的方式展示,並且標籤帶有移除功能,對應的核取方塊也要變更狀態。
  4. 實現值獲取。

第一步

根據觀察可以可以使用二維陣列,初始化時將整個根目錄push到陣列內,點選對應選項時,將子集push到陣列內,以此類推。直接上程式碼。

Typescript:

@Component({
    selector: 'directional-area-select',
    exportAs: 'directionalAreaSelect',
    templateUrl: './directional-select.component.html',
    styleUrls: ['./directional-select.component.less']
})
export class DirectionalSelectComponent implements OnInit{
    constructor() {
    }
    cacheList: any[] = [];
    _inputList;
    @Input('inputList') set inputList(value) {
        if(value instanceof Array && value.length){
            this._inputList = value;
            this.inputListChange();
        }
    }

    inputListChange() {
        if (this._inputList instanceof Array && this._inputList.length > 0) {
            this.cacheList.length = 0;
            this.cacheList.push(this._inputList);
        }
    }

    /**
     * 顯示對應的子集資料列表
     * @param index1 當前層數下標
     * @param index2 當前層數列表資料的下標
     * @param list 當前層的列表資料
     */
    pushCache(index1, index2, list) {
        //往後選擇
        let cl = this.cacheList[index1 + 1];
        let child = list[index2][this.child];
        if (child instanceof Array && child.length > 0) {
            if (!cl) {
                this.cacheList.push(child);
            } else {
                if (cl !== child) {
                    this.cacheList.splice(index1 + 1, 1, child)
                }
            }
        } else {
            if (cl !== child && !(child instanceof Array)) {
                this.cacheList.pop();
            }
        }

        //往前選擇
        if (child && child.length > 0) {
            while (this.cacheList.length > index1 + 2) {
                this.cacheList.pop();
            }
        }
    }
}複製程式碼

template:

<div class="select-list-inner">
    <div class="scope" *ngFor="let list of cacheList;let index1 = index" [ngStyle]="{'width.%':100.0 / cacheList.length}">
        <ul class="list-with-select">
            <li class="spaui" *ngFor="let l of list;let index2 = index" (click)="pushCache(index1,index2,list)">
                <app-checkbox [(ngModel)]="l.selected" [label]="l.name" [checkState]="l.checkState"></app-checkbox>
                <i *ngIf="l[child]?.length > 0" class="icon yc-icon">&#xe664;</i>
            </li>
        </ul>
    </div>
</div>
複製程式碼


逐步分析一下,  @Input('inputList') set inputList(value) {}  ,獲取傳入元件的值,即省市資料, inputListChange ,直接將整個資料push到 cacheList 裡面。這邊主要看看 pushCache 方法,使用者操作時,有可能向前選擇,也有可能向後選擇,這邊只要根據 cacheList 陣列長度,和傳進來的 index1 當前層數下標比較就能知道使用者的操作。

往後選擇也分三種情況

  • 同層級操作列表未出現下一層子集
  • 同層級操作列表以出現下一層子集
  • 同層級操作列表並沒有子集資料

第一種情況直接向 cacheList 陣列push子集

第二種情況將對應層級的資料替換新的子集內容

第三種情況判斷下層資料有值,並且對應層級列表沒有子集內容,移除陣列最後一項即可

往前選擇直接判斷 cacheList  長度和選擇對應的層級下標來移除 cacheList 次數即可。 

第二步

分析後,所有的選項都有核取方塊,所以每個都有肯能會有全選、未選、未全選的狀態。

需增加三個方法

自身改變,也要將狀態上下傳遞。

//選中有幾個狀態 對於父節點有 1全部選中 2部分選中 3全部取消 checkState 1 2 3
areaItemChange(data) {
    let child = data[this.child];
    if (data.selected) {
        data.checkState = 1
    } else {
        data.checkState = 3
    }

    //向下尋找
    if (child && child.length > 0) {
        this.recursionChildCheck(child)
    }
    //向上尋找
    this.recursionParentCheck(data);
}複製程式碼

通過遞迴的方式將子集的狀態與父級狀態同步

/**
 * 同步子集和父級的狀態
 * 遞迴
 * @param list
 */
private recursionChildCheck(list) {
    if (list && list.length > 0) {
        list.forEach(data => {
            let checked = data.parent.selected;
            data.selected = checked;
            if (checked) {
                data.checkState = 1;
                data.selected = true;
            } else {
                data.checkState = 3;
                data.selected = false;
            }
            let l = data[this.child];
            this.recursionChildCheck(l)
        })
    }
}
複製程式碼

通過計算父級下子集的被選狀態來確定父級最終狀態,length  選中的個數,length2 部分選中的個數,通過一下比較就能確定父級的最終狀態,一直遞迴到根元素。

/**
 * 判斷當前物件的父級中的子集被選中的個數和checkState == 2的個數來確定父級的當前狀態
 * 遞迴
 * @param data
 */
private recursionParentCheck(data) {
    let parent = data.parent;
    if (parent) {
        let l = parent[this.child];
        let length = l.reduce((previousValue, currentValue) => {
            return previousValue + ((currentValue.selected) ? 1 : 0)
        }, 0);
        let length2 = l.reduce((previousValue, currentValue) => {
            return previousValue + ((currentValue.checkState == 2) ? 1 : 0)
        }, 0);
        if (length == l.length) {
            parent.checkState = 1;
            parent.selected = true;
        } else if (length == 0 && length2 == 0) {
            parent.checkState = 3
        } else {
            parent.checkState = 2;
            parent.selected = false;
        }
        this.recursionParentCheck(parent);
    }
}複製程式碼

需要更改一下 inputListChange  方法

list
inputListChange() {
    if (this._inputList instanceof Array && this._inputList.length > 0) {
        this.list = this._inputList.map(d => {
            this.recursionChild(d);
            return d;
        });
        this.cacheList.length = 0;
        this.cacheList.push(this.list);
    }
}
複製程式碼

/**
 * 子集包含父級物件
 * 遞迴
 */
private recursionChild(target) {
    let list = target[this.child];
    if (list && list.length > 0) {
        list.forEach(data => {
            data.parent = target;
            this.recursionChild(data)
        })
    }
}
複製程式碼

這邊為了方便操作,在子元素都建立一個parent欄位儲存父級內容。

第三步

獲取被選的元素,將以標籤的形式顯示,如果父級的狀態為全選,就不需要考慮子集,直接顯示父級即可。

/**
 * 獲取被選的元素
 * 父級狀態為全選時,不需要考慮子集元素。
 */
private recursionResult(list, result = [], type = 1) {
    if (list && list.length > 0) {
        list.forEach(data => {
            //全部選中並且父級沒有核取方塊
            if ((data[this.hasCheckbox] && data.checkState == 1) || data.checkState == 2) {
                let child = data[this.child];
                if (child && child.length > 0) {
                    this.recursionResult(child, result, type);
                }
                //全部選中並且父級有核取方塊 結果不需要包含子集
            } else if (data.checkState == 1 && !data[this.hasCheckbox]) {
                switch (type) {
                    case 1:
                        result.push(data.id);
                        break;
                    case 2:
                        result.push({
                            id: data.id,
                            name: data.name,
                        });
                        break;
                    case 3:
                        result.push(data);
                        break;
                }
            }
        })
    }
    return result;
}
複製程式碼

標籤移除方法

removeResultList(data) {
    data.selected = false;
    this.areaItemChange(data);
}
複製程式碼

需要更改 areaItemChange 方法,核取方塊改變都需要重新計算 resultList 的值,這樣就達到了始終操作一個物件,改變對應標籤狀態,列表的狀態也會跟著改變。 

resultList
areaItemChange(data) {
    if (data[this.hasCheckbox]) return;

    let child = data[this.child];

    if (data.selected) {
        data.checkState = 1
    } else {
        data.checkState = 3
    }

    //向下尋找
    if (child && child.length > 0) {
        this.recursionChildCheck(child)
    }
    //向上尋找
    this.recursionParentCheck(data);
    this.resultList = this.recursionResult(this.list,[],3);
}
複製程式碼

第四步

獲取的值其實就是標籤裡的內容 (。•ˇ‸ˇ•。) 。


進階

可以改變元件的檢查策略,將後設資料 changeDetection 屬性 設定為 ChangeDetectionStrategy.OnPush 只有輸入屬性改變才會觸發檢查。  值型別改變也會觸發,引用型別只有引用改變才能觸發檢查。

可以將計算量比較大的程式碼另起一個後臺執行緒來處理,就以遞迴繫結父元素為例子。

private recursionChild(target) {
    let list = target[this.child];
    if (list && list.length > 0) {
        list.forEach(data => {
            data.parent = target;
            this.recursionChild(data)
        })
    }
}

/**
 * 採用Worker
 */
private recursionChildWorker(target,fn = ()=>{}){
    let fun = `
        onmessage = function (e) {
            let args = Array.from(e.data)
            let list = args[0];
            let key = args[1];
            function parent(target){
                let list = target[key];
                if (list && list.length > 0) {
                    list.forEach(data => {
                        data.parent = target;
                        this.parent(data);
                    })
                }
            }
            list.forEach(data => {
                parent(data);
            })
            postMessage(list);
        }
    `;
    const blob = new Blob([fun], {type: 'application/javascript'});
    const url = URL.createObjectURL(blob);
    const worker = new Worker(url);
    worker.postMessage([target, this.child]);
    worker.onmessage = () => {
        fn()
    }
}
複製程式碼


至此,這個元件算是完成了,如果有更好的寫法,歡迎留言,一起探討 ^_^。


相關文章