如何编写一个高性能的Angular组件

670 阅读6分钟
这篇文章讲述如何分析组建,编码技巧,如何巧妙的规避一些嵌套循环,减少嵌套循环,低性能的编码等。

案例

这次还是主要拿之前分享的一片文章中出现的业务组建,上次只是贴了代码,并没有详细说出实现过程,这次就以这个业务组建为中心,讲述如何编写一个高性能的业务组件。




根据上图分析组件所要完成的功能

这个类似省市联动的加强版,可以查看被勾选的省市,并且复选框都有三个状态,未选、全选、未全选,默认状态只显示根数据,点击相应的选项,如果有子集就会显示对应的子集数据。

需求分解

这边先将需求分解,一步一步的来实现功能

  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()
    }
}


至此,这个组件算是完成了,如果有更好的写法,欢迎留言,一起探讨 ^_^。