Home Reference Source Repository

src/Collection.js

/**
 * EMC (EFE Model & Collection)
 * Copyright 2014 Baidu Inc. All rights reserved.
 *
 * @file Collection类
 * @exports Collection
 * @author otakustay
 */
const EMPTY = {};
const SILENT = {silent: true};
const STORE = Symbol('store');

import EventTarget from 'mini-event/EventTarget';

/**
 * 一个带有集合变更通知的数组
 *
 * @class Collection
 * @extends mini-event.EventTarget
 *
 * @param {Array} [items] 初始化的数据
 *
 * @throws {Error} 提供的`items`参数不是数组
 */
export default class Collection extends EventTarget {
    constructor(items) {
        super();
        this[STORE] = [];
        this.length = 0;

        if (items) {
            this.addArray(items, SILENT);
        }
    }

    /**
     * 获取集合中指定位置上的元素
     *
     * @method Collection#get
     *
     * @param {number} index 指定位置,如果为负数则从元素最后开始往前计算
     * @return {*} 指定位置的元素,如果指定的位置超出集合范围,则返回`undefined`
     *
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`index`参数
     * @throws {Error} 提供的`index`参数无法转换为数字
     */
    get(index) {
        if (!this[STORE]) {
            throw new Error('This collection is disposed');
        }

        if (index == null) {
            throw new Error('Argument index is not provided');
        }

        index = +index;

        if (isNaN(index)) {
            throw new Error('Argument index cannot convert to a number');
        }

        // 如果是空的就直接返回,避免太多计算
        if (!this.length) {
            return undefined;
        }

        // 由于超出范围后要返回`undefined`,此处不能用`getValidIndex`来将`index`计算至可用范围
        if (index < 0) {
            index = this.length + index;
        }
        if (index < 0 || index >= this.length) {
            return undefined;
        }

        let item = this[STORE][index];
        return item;
    }

    /**
     * 在指定位置添加一个元素
     *
     * @method Collection#insert
     *
     * @param {number} index 需要添加的位置,关于位置的计算参考{@link Collection#getValidIndex}
     * @param {*} item 需要添加的元素
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:add}事件
     * @return {*} 插入的元素
     *
     * @fires add
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`index`参数
     * @throws {Error} 提供的`index`参数无法转换为数字
     * @throws {Error} 未提供`item`参数
     */
    insert(index, item, options) {
        if (!this[STORE]) {
            throw new Error('This collection is disposed');
        }

        switch (arguments.length) {
            case 0:
                throw new Error('Argument index is not provided');
            case 1:
                throw new Error('Argument item is not provided');
        }

        index = this.getValidIndex(index);
        options = options || EMPTY;

        // 没有必要特地优化为`push`和`unshift`:http://jsperf.com/push-vs-splice
        this[STORE].splice(index, 0, item);
        this.length = this[STORE].length;

        if (!options.silent) {
            /**
             * 添加元素时触发
             *
             * @event Collection#add
             *
             * @property {number} index 添加元素的位置
             * @property {*} item 添加的元素
             */
            this.fire('add', {index: index, item: item});
        }

        return item;
    }

    /**
     * 在指定位置添加一个元素,与{@link Collection#insert}方法相同
     *
     * @method Collection#addAt
     *
     * @param {number} index 需要添加的位置,关于位置的计算参考{@link Collection#getValidIndex}
     * @param {*} item 需要添加的元素
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:add|add事件}
     * @return {*} 插入的元素
     *
     * @fires add
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`index`参数
     * @throws {Error} 提供的`index`参数无法转换为数字
     * @throws {Error} 未提供`item`参数
     */
    addAt(index, item, options) {
        return this.insert(index, item, options);
    }

    /**
     * 在当前集合的最后位置添加一个元素
     *
     * @method Collection#add
     *
     * @param {*} item 需要添加的元素
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:add|add事件}
     * @return {*} 插入的元素
     *
     * @fires add
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`item`参数
     */
    add(item, options) {
        if (!arguments.length) {
            throw new Error('Argument item is not provided');
        }

        return this.insert(this.length, item, options);
    }

    /**
     * 在当前集合的最后位置添加一个元素,与{@link Collection#add}方法相同
     *
     * @method Collection#push
     *
     * @param {*} item 需要添加的元素
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:add|add事件}
     * @return {*} 插入的元素
     *
     * @fires add
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`item`参数
     */
    push(item, options) {
        return this.add(item, options);
    }

    /**
     * 在当前集合的最前位置添加一个元素
     *
     * @method Collection#unshift
     *
     * @param {*} item 需要添加的元素
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:add|add事件}
     * @return {*} 插入的元素
     *
     * @fires add
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`item`参数
     */
    unshift(item, options) {
        if (!arguments.length) {
            throw new Error('Argument item is not provided');
        }

        return this.insert(0, item, options);
    }

    /**
     * 从指定位置移除一个元素
     *
     * @method Collection#removeAdd
     *
     * @param {number} index 需要移除的元素的位置,关于位置的计算参考{@link Collection#getValidIndex}
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:remove|remvoe事件}
     *
     * @fires remove
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`index`参数
     * @throws {Error} 提供的`index`参数无法转换为数字
     */
    removeAt(index, options) {
        if (!this[STORE]) {
            throw new Error('This collection is disposed');
        }

        let actualIndex = Math.min(this.length - 1, this.getValidIndex(index));
        options = options || EMPTY;

        // 空的就不用计算了,否则会触发事件,为了保持异常的一致性,要先计算`index`确认不用抛出异常,因此不要放到前面去
        if (!this.length) {
            return;
        }

        let removedItem = this[STORE].splice(actualIndex, 1)[0];
        this.length = this[STORE].length;

        if (!options.silent) {
            /**
             * 移除元素时触发
             *
             * @event Collection#remove
             *
             * @property {number} index 移除元素的位置
             * @property {*} item 移除的元素
             */
            this.fire('remove', {index: actualIndex, item: removedItem});
        }
    }

    /**
     * 移除集合最后一个元素并返回
     *
     * @method Collection#pop
     *
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:remove|remove事件}
     * @return {*} 集合中的最后一个元素
     *
     * @fires remove
     * @throws {Error} 当前集合已经销毁
     */
    pop(options) {
        let lastItem = this.get(-1);
        this.removeAt(-1, options);
        return lastItem;
    }

    /**
     * 移除集合第一个元素并返回
     *
     * @method Collection#shift
     *
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:remove|remove事件}
     * @return {*} 集合中的第一个元素
     *
     * @fires remove
     * @throws {Error} 当前集合已经销毁
     */
    shift(options) {
        let firstItem = this.get(0);
        this.removeAt(0, options);
        return firstItem;
    }

    /**
     * 移除集合中所有的给定元素
     *
     * @method Collection#remove
     *
     * @param {*} item 需要移除的元素
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:remove|remove事件}
     *
     * @fires remove
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`item`参数
     */
    remove(item, options) {
        if (!arguments.length) {
            throw new Error('Argument item is not provided');
        }

        let startIndex = this.indexOf(item);
        while (startIndex !== -1) {
            this.removeAt(startIndex, options);
            startIndex = this.indexOf(item, startIndex);
        }
    }

    /**
     * 查找指定元素在集合中第一次出现的位置
     *
     * @method Collection#indexOf
     *
     * @param {*} item 指定查找的元素
     * @param {number} [startIndex=0] 指定开始查找的位置,如果为负数则从最后位置往前计算,如果超出范围则不进行搜索返回`-1`
     * @return {number} 元素所在的位置,如果集合中从指定的位置开始未能找到元素则返回-1
     *
     * @throws {Error} 当前集合已经销毁
     * @throws {Error} 未提供`item`参数
     * @throws {Error} 提供的`startIndex`参数无法转换为数字
     */
    indexOf(item, startIndex) {
        if (!this[STORE]) {
            throw new Error('This collection is disposed');
        }

        if (!arguments.length) {
            throw new Error('Argument item is not provided');
        }

        startIndex = startIndex || 0;

        return this[STORE].indexOf(item, startIndex);
    }

    /**
     * 导出当前集合为普通的数组
     *
     * 如果当前集合已经销毁,该方法会返回一个空数组`[]`
     *
     * @method Collection#dump
     *
     * @return {Array} 包含当前集合的元素(及其顺序)的数组
     */
    dump() {
        return this[STORE] ? this[STORE].slice() : [];
    }

    /**
     * 复制当前集合
     *
     * @method Collection#clone
     *
     * @return {Collection} 一个新的集合,包含当前集合的元素(及其顺序)
     *
     * @throws {Error} 当前集合已经销毁
     */
    clone() {
        if (!this[STORE]) {
            throw new Error('This collection is disposed');
        }

        let Collection = this.constructor;
        return new Collection(this[STORE]);
    }

    /**
     * 销毁当前集合
     *
     * @method Collection#dispose
     */
    dispose() {
        this.destroyEvents();
        this[STORE] = null;
        this.length = undefined;
    }

    /**
     * 获取一个可用的索引值
     *
     * - 当给定的索引大于集合长度时,返回集合的长度
     * - 当给定的索引小于0时,从集合的末尾开始向前计算
     * - 当索引小于0并且其绝对值大于集合长度时,返回0
     *
     * 如果用于删除,由于索引值过大时会返回当前集合的长度,需要在返回值之后再减去1得到正确的删除位置
     *
     * @method Collection#getValidIndex
     *
     * @param {number} index 输入的索引值
     * @return {number} 计算后的可用索引值
     * @protected
     *
     * @throws {Error} 未提供`index`参数
     * @throws {Error} 提供的`index`参数无法转换为数字
     */
    getValidIndex(index) {
        if (index == null) {
            throw new Error('Argument index is not provided');
        }

        let validIndex = +index;

        if (isNaN(validIndex)) {
            throw new Error('Argument index (of value "' + index + '") cannot convert to a number');
        }

        if (validIndex > this.length) {
            return this.length;
        }

        if (validIndex < 0) {
            return Math.max(this.length + validIndex, 0);
        }

        return validIndex;
    }

    /**
     * 添加一系列的元素
     *
     * 此方法为私有方法,不要由外部或子类调用
     *
     * @method Collection#addArray
     *
     * @param {Array} items 需要添加的元素数组
     * @param {Object} [options] 相关选项
     * @param {boolean} [options.silent=false] 如果该值为`true`则不触发{@link Collection#event:add}事件
     * @protected
     *
     * @throws {Error} 提供的`items`参数不是数组
     */
    addArray(items, options) {
        if (typeof items[Symbol.iterator] !== 'function') {
            throw new Error('Argument itmes (of value "' + items + '") is not iterable');
        }

        for (let item of items) {
            this.add(item, options);
        }
    }
}