CKEditor5——Utils(Collection类理解)

CKEditor5——Utils(Collection类理解)

今天我们学习CK5的Utils工具包中比较重要且常用的一个关键类:Collection,先贴出比较关键的代码:

export default class Collection {
 	constructor( initialItemsOrOptions = {}, options = {} ) {
		const hasInitialItems = isIterable( initialItemsOrOptions );

		if ( !hasInitialItems ) {
			options = initialItemsOrOptions;
		}

		/**
		 * The internal list of items in the collection.
		 *
		 * @private
		 * @member {Object[]}
		 */
		this._items = [];

		/**
		 * The internal map of items in the collection.
		 *
		 * @private
		 * @member {Map}
		 */
		this._itemMap = new Map();

		/**
		 * The name of the property which is considered to identify an item.
		 *
		 * @private
		 * @member {String}
		 */
		this._idProperty = options.idProperty || 'id';

		/**
		 * A helper mapping external items of a bound collection ({@link #bindTo})
		 * and actual items of this collection. It provides information
		 * necessary to properly remove items bound to another collection.
		 *
		 * See {@link #_bindToInternalToExternalMap}.
		 *
		 * @protected
		 * @member {WeakMap}
		 */
		this._bindToExternalToInternalMap = new WeakMap();

		/**
		 * A helper mapping items of this collection to external items of a bound collection
		 * ({@link #bindTo}). It provides information necessary to manage the bindings, e.g.
		 * to avoid loops in two–way bindings.
		 *
		 * See {@link #_bindToExternalToInternalMap}.
		 *
		 * @protected
		 * @member {WeakMap}
		 */
		this._bindToInternalToExternalMap = new WeakMap();

		/**
		 * Stores indexes of skipped items from bound external collection.
		 *
		 * @private
		 * @member {Array}
		 */
		this._skippedIndexesFromExternal = [];

		// Set the initial content of the collection (if provided in the constructor).
		if ( hasInitialItems ) {
			for ( const item of initialItemsOrOptions ) {
				this._items.push( item );
				this._itemMap.set( this._getItemIdBeforeAdding( item ), item );
			}
		}

		/**
		 * A collection instance this collection is bound to as a result
		 * of calling {@link #bindTo} method.
		 *
		 * @protected
		 * @member {module:utils/collection~Collection} #_bindToCollection
		 */
	}
}

从上面的代码可以看出有两个关键的字段就是: this._items,this._itemMap一个数组和一个Map,所以可以猜测collection可以方便的插入数据和快速查询。

从构造函数可以看出,它接收两个值,第一个值是一个可以迭代的对象,另一个是配置对象。我们看看它的具体用法:

const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] );

console.log( collection.get( 0 ) ); // -> { id: 'John' }
console.log( collection.get( 1 ) ); // -> { id: 'Mike' }
console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' }
// or add item
const collection = new Collection();

collection.add( { id: 'John' } );
console.log( collection.get( 0 ) ); // -> { id: 'John' }
//这里是往connection添加了数组对象

//这里是通过最后一个参数来配置一些属性比如集合的idProperty属性,这样在查找的时候就可以
//根据这个属性进行查找
const emptyCollection = new Collection( { idProperty: 'name' } );
emptyCollection.add( { name: 'John' } );
console.log( collection.get( 'John' ) ); // -> { name: 'John' }

const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } );
nonEmptyCollection.add( { name: 'George' } );
console.log( collection.get( 'George' ) ); // -> { name: 'George' }
console.log( collection.get( 'John' ) ); // -> { name: 'John' }

 

我们再来看看另外两个关键的属性:this._bindToInternalToExternalMapthis._bindToInternalToExternalMap这是两个有意思的属性,根据字面意思猜测这两个属性是用在bindTo这个方法中的,而且这两个Map构成了一个双向映射。即从内部到外部的映射和从外部到内部的映射。

下面我们看看这个方法:

bindTo( externalCollection ) {
	if ( this._bindToCollection ) {
		/**
		 * The collection cannot be bound more than once.
		 *
		 * @error collection-bind-to-rebind
		 */
		throw new CKEditorError( 'collection-bind-to-rebind', this );
	}

	this._bindToCollection = externalCollection;

	return {
		as: Class => {
			this._setUpBindToBinding( item => new Class( item ) );
		},

		using: callbackOrProperty => {
			if ( typeof callbackOrProperty == 'function' ) {
				this._setUpBindToBinding( item => callbackOrProperty( item ) );
			} else {
				this._setUpBindToBinding( item => item[ callbackOrProperty ] );
			}
		}
	};
}

这个方法的作用是绑定当前集合与外部集合,让它们的数据能够同步,我们看看用法:

class FactoryClass {
	constructor( data ) {
		this.label = data.label;
	}
}

const source = new Collection( { idProperty: 'label' } );
const target = new Collection();

target.bindTo( source ).as( FactoryClass );

source.add( { label: 'foo' } );
source.add( { label: 'bar' } );

console.log( target.length ); // 2
console.log( target.get( 1 ).label ); // 'bar'

source.remove( 0 );
console.log( target.length ); // 1
console.log( target.get( 0 ).label ); // 'bar'

注意到没,我们这里有一个集合source,它的id属性叫做label,另一个集合叫做target,通过调用

target.bindTo( source ).as( FactoryClass );

这样source就将source与target进行了绑定,一旦修改了source的值,比如添加之后,那么target也会跟着同步,这里有一点需要注意的是,尽管source和target都是集合,但是它们里面存储的item是不一样的,source存储的是普通对象,但是target存储的FactoryClass类型的对象,且这些对象有共同的属性就是label。

我们再看bindTo这个方法,其实关键的就是this._setUpBindToBinding( item => new Class( item ) );

/**
 * Finalizes and activates a binding initiated by {#bindTo}.
 *
 * @protected
 * @param {Function} factory A function which produces collection items.
 */
_setUpBindToBinding( factory ) {
	const externalCollection = this._bindToCollection;

	// Adds the item to the collection once a change has been done to the external collection.
	//
	// @private
	const addItem = ( evt, externalItem, index ) => {
		const isExternalBoundToThis = externalCollection._bindToCollection == this;
		const externalItemBound = externalCollection._bindToInternalToExternalMap.get( externalItem );

		// If an external collection is bound to this collection, which makes it a 2–way binding,
		// and the particular external collection item is already bound, don't add it here.
		// The external item has been created **out of this collection's item** and (re)adding it will
		// cause a loop.
		if ( isExternalBoundToThis && externalItemBound ) {
			this._bindToExternalToInternalMap.set( externalItem, externalItemBound );
			this._bindToInternalToExternalMap.set( externalItemBound, externalItem );
		} else {
			const item = factory( externalItem );

			// When there is no item we need to remember skipped index first and then we can skip this item.
			if ( !item ) {
				this._skippedIndexesFromExternal.push( index );

				return;
			}

			// Lets try to put item at the same index as index in external collection
			// but when there are a skipped items in one or both collections we need to recalculate this index.
			let finalIndex = index;

			// When we try to insert item after some skipped items from external collection we need
			// to include this skipped items and decrease index.
			//
			// For the following example:
			// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]
			// internal -> [ A ]
			//
			// Another item is been added at the end of external collection:
			// external.add( 'D' )
			// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]
			//
			// We can't just add 'D' to internal at the same index as index in external because
			// this will produce empty indexes what is invalid:
			// internal -> [ 'A', empty, empty, 'D' ]
			//
			// So we need to include skipped items and decrease index
			// internal -> [ 'A', 'D' ]
			for ( const skipped of this._skippedIndexesFromExternal ) {
				if ( index > skipped ) {
					finalIndex--;
				}
			}

			// We need to take into consideration that external collection could skip some items from
			// internal collection.
			//
			// For the following example:
			// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]
			// external -> [ A ]
			//
			// Another item is been added at the end of external collection:
			// external.add( 'D' )
			// external -> [ 'A', 'D' ]
			//
			// We need to include skipped items and place new item after them:
			// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]
			for ( const skipped of externalCollection._skippedIndexesFromExternal ) {
				if ( finalIndex >= skipped ) {
					finalIndex++;
				}
			}

			this._bindToExternalToInternalMap.set( externalItem, item );
			this._bindToInternalToExternalMap.set( item, externalItem );
			this.add( item, finalIndex );

			// After adding new element to internal collection we need update indexes
			// of skipped items in external collection.
			for ( let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++ ) {
				if ( finalIndex <= externalCollection._skippedIndexesFromExternal[ i ] ) {
					externalCollection._skippedIndexesFromExternal[ i ]++;
				}
			}
		}
	};

	// Load the initial content of the collection.
	for ( const externalItem of externalCollection ) {
		addItem( null, externalItem, externalCollection.getIndex( externalItem ) );
	}

	// Synchronize the with collection as new items are added.
	this.listenTo( externalCollection, 'add', addItem );

	// Synchronize the with collection as new items are removed.
	this.listenTo( externalCollection, 'remove', ( evt, externalItem, index ) => {
		const item = this._bindToExternalToInternalMap.get( externalItem );

		if ( item ) {
			this.remove( item );
		}

		// After removing element from external collection we need update/remove indexes
		// of skipped items in internal collection.
		this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce( ( result, skipped ) => {
			if ( index < skipped ) {
				result.push( skipped - 1 );
			}

			if ( index > skipped ) {
				result.push( skipped );
			}

			return result;
		}, [] );
	} );
}

首先我们看到的是有一个关键属性this._bindToCollection,这个属性就是当前集合绑定的需要同步数据的外部集合。然后迭代外部集合,将外部集合的数据添加到当前集合。添加集合的逻辑是一个私有方法addItem,其实就是建立外部集合和内部集合的映射。最后就是绑定外部集合的add和remove事件,也就是一旦外部集合添加或者移出数据,那么这里的回调函数就会触发,从而添加当前集合数据或者删除。

这里具体的逻辑也很容易,就是当绑定的数据存在的时候只是添加双向映射,而绑定的数据不存在的时候,调用工厂函数创建并且添加双向映射。有兴趣的可以详细按照各种情况进行列举分析。

最后,我们要谈到的是两个方法,那就是addremove

add( item, index ) {
	return this.addMany( [ item ], index );
}

/**
 * Adds multiple items into the collection.
 *
 * Any item not containing an id will get an automatically generated one.
 *
 * @chainable
 * @param {Iterable.<Object>} item
 * @param {Number} [index] The position of the insertion. Items will be appended if no `index` is specified.
 * @fires add
 * @fires change
 */
addMany( items, index ) {
	if ( index === undefined ) {
		index = this._items.length;
	} else if ( index > this._items.length || index < 0 ) {
		/**
		 * The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
		 * is invalid. It must be a number between 0 and the collection's length.
		 *
		 * @error collection-add-item-invalid-index
		 */
		throw new CKEditorError( 'collection-add-item-invalid-index', this );
	}

	for ( let offset = 0; offset < items.length; offset++ ) {
		const item = items[ offset ];
		const itemId = this._getItemIdBeforeAdding( item );
		const currentItemIndex = index + offset;

		this._items.splice( currentItemIndex, 0, item );
		this._itemMap.set( itemId, item );

		this.fire( 'add', item, currentItemIndex );
	}

	this.fire( 'change', {
		added: items,
		removed: [],
		index
	} );

	return this;
}
remove( subject ) {
	const [ item, index ] = this._remove( subject );

	this.fire( 'change', {
		added: [],
		removed: [ item ],
		index
	} );

	return item;
}

这里我们注意到这两个方法的逻辑除了操作this._itemsthis._itemMap这两个内部的基本数据结构外,还触发了add,remove,以及change事件,正式由于这个简单的功能,让collection变成了一个基于事件驱动的集合,因此可以监听集合的这些事件,从而做一些自定义的处理。为什么集合有这样的功能呢?关键的代码在这里:

mix( Collection, EmitterMixin );

好了,做了以上分析以后,大家应该对这个Collection的功能都清晰了吧。

总结如下:

1、Collection是一个基本的数据集合容器,能方便的添加,删除以及查询数据。

2、Collection是一个基于事件驱动的集合。

3、Collection是一个能够方便的在两个集合(source-->target)之间同步数据的容器。

版权声明:著作权归作者所有。

thumb_up 0 | star_outline 0 | textsms 0