敲碎时间,铸造不朽的个人专栏
上一篇

CKEditor5——Template理解(二)

广告
选中文字可对指定文章内容进行评论啦,→和←可快速切换按钮,绿色背景文字可以点击查看评论额。
大纲

CKEditor5——Template理解(二)

在上一节我们分析了类Template的构造函数,今天我们继续学习其他的方法:

我们先看看render()方法

render() {
	const node = this._renderNode( {
		intoFragment: true
	} );

	this._isRendered = true;

	return node;
}
// const domNode = new Template( { ... } ).render(); 然后将此节点添加到某个dom,然后
// 就可以操作此dom节点啦。

这个方法的主要作用就是渲染出来一个dom节点,然后设置当前的模板this._isRendered = true,并且返回当前节点。为了理解这个方法,我们需要理解this._renderNode(data)

_renderNode( data ) {
	let isInvalid;

	if ( data.node ) {
		// When applying, a definition cannot have "tag" and "text" at the same time.
		isInvalid = this.tag && this.text;
	} else {
		// When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
		isInvalid = this.tag ? this.text : !this.text;
	}

	if ( isInvalid ) {
		/**
		 * Node definition cannot have the "tag" and "text" properties at the same time.
		 * Node definition must have either "tag" or "text" when rendering a new Node.
		 *
		 * @error ui-template-wrong-syntax
		 */
		throw new CKEditorError(
			'ui-template-wrong-syntax',
			this
		);
	}

	if ( this.text ) {
		return this._renderText( data );
	} else {
		return this._renderElement( data );
	}
}

从这个方法,我们可以看出,首先会判断节点的数据是否合法,这里有一个逻辑就是tag和text属性不能同时存在,如果同时存在就认为是不合法的,并且还有一点需要注意的是:这个方法会被render(data)apply(data)调用,它们的判断逻辑是不一样的。剩下的逻辑就是如果当前的节点是text,那么就调用this._renderText( data ),否则调用this._renderElement( data )

下面我们看看this._renderText( data )

_renderText( data ) {
	let node = data.node;

	// Save the original textContent to revert it in #revert().
	if ( node ) {
		data.revertData.text = node.textContent;
	} else {
		node = data.node = document.createTextNode( '' );
	}

	// Check if this Text Node is bound to Observable. Cases:
	//
	//		text: [ Template.bind( ... ).to( ... ) ]
	//
	//		text: [
	//			'foo',
	//			Template.bind( ... ).to( ... ),
	//			...
	//		]
	//
	if ( hasTemplateBinding( this.text ) ) {
		this._bindToObservable( {
			schema: this.text,
			updater: getTextUpdater( node ),
			data
		} );
	}
	// Simply set text. Cases:
	//
	//		text: [ 'all', 'are', 'static' ]
	//
	//		text: [ 'foo' ]
	//
	else {
		node.textContent = this.text.join( '' );
	}

	return node;
}

我们看这个方法,可以知道,首先会将文本节点的数据存在在一个叫做data.revertData.text,这个属性revertData属性的作用我们暂时不用理解,后面介绍applyrevert方法的时候才来学习这个属性。如果node不存在就创建一个文本节点,并且将文本属性这是到node.textContent。这里还有一个逻辑就是检查当前的文本节点是否绑定了Observable,如果有这样的绑定,那么需要执行对文本节点的绑定添加事件,this._bindToObservable( { schema: this.text, updater: getTextUpdater( node ), data } );

我们再看看以上的这个方法,不过呢?我们先看看hasTemplateBinding( )这个方法

function hasTemplateBinding( schema ) {
	if ( !schema ) {
		return false;
	}

	// Normalize attributes with additional data like namespace:
	//
	//		class: {
	//			ns: 'abc',
	//			value: [ ... ]
	//		}
	//
	if ( schema.value ) {
		schema = schema.value;
	}

	if ( Array.isArray( schema ) ) {
		return schema.some( hasTemplateBinding );
	} else if ( schema instanceof TemplateBinding ) {
		return true;
	}

	return false;
}

这里的逻辑比较简单,我拿this.text来举例哈,如果此属性包含类似:

text: [
	//			'foo',
	//			Template.bind( ... ).to( ... ),
	//			...
	//		
]

比如,以上就是一个包含TemplateBinding的数组,因此这样的情况就需要处理绑定方法:

_bindToObservable( { schema, updater, data } ) {
	const revertData = data.revertData;

	// Set initial values.
	syncValueSchemaValue( schema, updater, data );

	const revertBindings = schema
		// Filter "falsy" (false, undefined, null, '') value schema components out.
		.filter( item => !isFalsy( item ) )
		// Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
		.filter( item => item.observable )
		// Once only the actual binding are left, let the emitter listen to observable change:attribute event.
		// TODO: Reduce the number of listeners attached as many bindings may listen
		// to the same observable attribute.
		.map( templateBinding => templateBinding.activateAttributeListener( schema, updater, data ) );

	if ( revertData ) {
		revertData.bindings.push( revertBindings );
	}
}

这个方法看着比较复杂,但是我们分三步来看:

1、首先拿出revertData属性,然后将此属性的bindings添加一个revertBindings,这个是个啥呢,我告诉你,这个实际上就是一个数组,并且数组中包含的值是函数。

2、其次就是syncValueSchemaValue这个方法,作用就是初始化一些值,

3、最后就是对参数的schema进行处理,首先过滤掉一些为false的对象,然后再筛选出包含observable的schema,最后就是剩下的templateBinding执行添加属性监听器。

我们先看看syncValueSchemaValue这个函数

function syncValueSchemaValue( schema, updater, { node } ) {
	let value = getValueSchemaValue( schema, node );

	// Check if schema is a single Template.bind.if, like:
	//
	//		class: Template.bind.if( 'foo' )
	//
	if ( schema.length == 1 && schema[ 0 ] instanceof TemplateIfBinding ) {
		value = value[ 0 ];
	} else {
		value = value.reduce( arrayValueReducer, '' );
	}

	if ( isFalsy( value ) ) {
		updater.remove();
	} else {
		updater.set( value );
	}
}
function getValueSchemaValue( schema, node ) {
	return schema.map( schemaItem => {
		// Process {@link module:ui/template~TemplateBinding} bindings.
		if ( schemaItem instanceof TemplateBinding ) {
			return schemaItem.getValue( node );
		}

		// All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
		return schemaItem;
	} );
}
function getTextUpdater( node ) {
	return {
		set( value ) {
			node.textContent = value;
		},

		remove() {
			node.textContent = '';
		}
	};
}

我们需要理解这个参数updater这个有函数getTextUpdater知道这个实际上是一个对象,包含两个方法,设置值和移除值,就是对当前的文本节点设置值和将当前节点的值设置为空字符串。我们根据逻辑知道

getValueSchemaValue这个函数返回的值为false,则将当前节点的文本属性设置为空,否则就将此值修改为新的值。同时我们也知道这个返回值是一个数组,这个数组的值是怎么来的呢?

getValueSchemaValue的具体实现可以看出,如果schemaItem只是普通文本,那么直接返回,如果是TemplateBinding,那么需要获取新的值,然后对这些值进行处理后设置。设置的时候分为数组长度为1或者数组长度大于1的情况,数组长度为1的时候就直接取第一个值,否则就是将这这些值连起来。

比如:

{
    text: [
		'aaa'
    ]
}
//以上就是数组是一个值的情况
{
    text: [
		'aaa',
		'bbb'
    ]
}
//以上就是数组是多个值的情况

为了理解以上情况,我们需要知道什么是TemplateBinding

export class TemplateBinding {
	/**
	 * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
	 *
	 * @param {module:ui/template~TemplateDefinition} def The definition of the binding.
	 */
	constructor( def ) {
		Object.assign( this, def );

		/**
		 * An observable instance of the binding. It either:
		 *
		 * * provides the attribute with the value,
		 * * or passes the event when a corresponding DOM event is fired.
		 *
		 * @member {module:utils/observablemixin~ObservableMixin} module:ui/template~TemplateBinding#observable
		 */

		/**
		 * An {@link module:utils/emittermixin~Emitter} used by the binding to:
		 *
		 * * listen to the attribute change in the {@link module:ui/template~TemplateBinding#observable},
		 * * or listen to the event in the DOM.
		 *
		 * @member {module:utils/emittermixin~EmitterMixin} module:ui/template~TemplateBinding#emitter
		 */

		/**
		 * The name of the {@link module:ui/template~TemplateBinding#observable observed attribute}.
		 *
		 * @member {String} module:ui/template~TemplateBinding#attribute
		 */

		/**
		 * A custom function to process the value of the {@link module:ui/template~TemplateBinding#attribute}.
		 *
		 * @member {Function} [module:ui/template~TemplateBinding#callback]
		 */
	}

	/**
	 * Returns the value of the binding. It is the value of the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}. The value may be processed by the
	 * {@link module:ui/template~TemplateBinding#callback}, if such has been passed to the binding.
	 *
	 * @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
	 * @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}.
	 */
	getValue( node ) {
		const value = this.observable[ this.attribute ];

		return this.callback ? this.callback( value, node ) : value;
	}

	/**
	 * Activates the listener which waits for changes of the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}, then updates the DOM with the aggregated
	 * value of {@link module:ui/template~TemplateValueSchema}.
	 *
	 * @param {module:ui/template~TemplateValueSchema} schema A full schema to generate an attribute or text in the DOM.
	 * @param {Function} updater A DOM updater function used to update the native DOM attribute or text.
	 * @param {module:ui/template~RenderData} data Rendering data.
	 * @returns {Function} A function to sever the listener binding.
	 */
	activateAttributeListener( schema, updater, data ) {
		const callback = () => syncValueSchemaValue( schema, updater, data );

		this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );

		// Allows revert of the listener.
		return () => {
			this.emitter.stopListening( this.observable, 'change:' + this.attribute, callback );
		};
	}
}
export class TemplateToBinding extends TemplateBinding {
	/**
	 * Activates the listener for the native DOM event, which when fired, is propagated by
	 * the {@link module:ui/template~TemplateBinding#emitter}.
	 *
	 * @param {String} domEvtName The name of the native DOM event.
	 * @param {String} domSelector The selector in the DOM to filter delegated events.
	 * @param {module:ui/template~RenderData} data Rendering data.
	 * @returns {Function} A function to sever the listener binding.
	 */
	activateDomEventListener( domEvtName, domSelector, data ) {
		const callback = ( evt, domEvt ) => {
			if ( !domSelector || domEvt.target.matches( domSelector ) ) {
				if ( typeof this.eventNameOrFunction == 'function' ) {
					this.eventNameOrFunction( domEvt );
				} else {
					this.observable.fire( this.eventNameOrFunction, domEvt );
				}
			}
		};

		this.emitter.listenTo( data.node, domEvtName, callback );

		// Allows revert of the listener.
		return () => {
			this.emitter.stopListening( data.node, domEvtName, callback );
		};
	}
}
export class TemplateIfBinding extends TemplateBinding {
	/**
	 * @inheritDoc
	 */
	getValue( node ) {
		const value = super.getValue( node );

		return isFalsy( value ) ? false : ( this.valueIfTrue || true );
	}

	/**
	 * The value of the DOM attribute or text to be set if the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable} is `true`.
	 *
	 * @member {String} [module:ui/template~TemplateIfBinding#valueIfTrue]
	 */
}

以上是TemplateBinding的实现代码,我们可以看出它有两个关键方法activateAttributeListenergetValue方法,同时这个类有两个继承类,TemplateToBinding对应的实际上就是bind.to(),而TemplateIfBinding对应的就是bind.if()

我们这里需要先看看构造函数:

构造函数里实际上包含多个属性:observableemitterattributecallback,在执行activateAttributeListener的时候,实际上就是监听observable的属性的变化,如果属性有变化,那么这个变化就会同步到dom节点上去。并且返回一个函数,这个函数就是为了取消这种监听。前面我们提到了返回的是一个函数数组,在那里的函数实际上就是这里的返回函数,作用就是revert的时候取消这样的监听。

同时这个类有一个getValue方法,这个方法就是首先获得observable的属性值,如果有回调函数,那么就调用回调函数,否则直接返回属性的值。

{
	text: bind.to( 'b', ( value, node ) => value.toUpperCase() )
}
//这里就是有对调函数的情况,获取的值是通过对调函数来实现的。

好了,今天我们讲解了render()方法中的_renderText()的具体实现,并且简单介绍了绑定是怎么实现的。

另外一个就是_renderElement方法的实现,我打算下一节来继续分析。欢迎讨论。

 

 

 

 

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