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方法的实现,我打算下一节来继续分析。欢迎讨论。

 

 

 

 

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

相关推荐

Angular 6.0.0 break change:移除<template>标签

为了避免冲突,Angular v4废弃了<template>标签。Angular 6.0.0则正式移除了<template>标签,<template>标签需要使用<ng-template>替换。Angular 4在tsconfig.json提供了angularCompilerOptions的编译选项enableLegacyTemplate来兼容<

CKEditor5事件系统(代理事件)

emitter接口提供了事件代理机制。也就是说指定选择的事件能够被其他的emitter触发。 1、代理指定的事件到另一个emitterlet anyClass = new AnyClass(); let anotherClass = new AnyClass(); let oneClass = new AnyClass(); anotherClass.on('bar',(evt,data

RxJs——Subject理解二

上一节我们对subject有了初步理解,今天我们继续学习subjectReplaySubject介绍这个ReplaySubject之前,我们说下一种使用场景1、我们创建一个subject2、在应用的某个地方,我们向subject推送值,但此时没有订阅3、在某个时点,有第一个观察者开始订阅4、我们期望观察者能接收之前主题推送的值(可能是全部值或者时其中一个)5、实际上啥都没发生,因为主题不能存储记忆

CKEditor5——模型理解(一)

我们知道,CK5实现了一个MVC的架构,从今天开始,我们一步一步深入学习模型,视图,以及模型和视图之间的转换。今天我们开始模型的学习。首先,我们看模型的定义:The model is implemented by a DOM-like tree structure of elements and text nodes.模型由两类节点构成,分别是元素节点和文本节点,模型是一种类Dom树结构。我们知道

RxJs——错误处理(二)

从前文我们知道,错误处理有两种方式,一种是重新抛出一个错误,另一个是提供一个默认的回调值。今天我们介绍错误处理的另一种方式,先来看看重试。重试策略有一点需要记住,一旦流出现了错误,我们不能恢复它。但是没有什么能阻碍我们订阅其派生类对应的Observable,并且创建一个新的流。这种策略的工作原理是:1、我们将获取输入Observable并且订阅它,这将创建一个新的流。2、如果流没有出错,我们将在输

CKEditor5——模型理解(二:Node)

上一节我们理解了基本的CK5的模型基本信息,今天我们来学习一些模型的API。节点说明首先,需要理解的就是模型的节点。在这一点上,CK5的模型节点和dom的节点有点类似,也有一些不同。我会在文章中一一介绍。节点是模型树的基本结构。它是模型中不同节点类型的一种抽象。这里需要指出的一点是:如果一个节点从模型树中分离出来,你可以使用它的 API 来操作它。但是,非常重要的是,已经附加到模型树的节点只能通过

CKEditor5——模型理解(三:Element Text)

在上一节,我们学习了CK5中模型节点Node的API,今天我们学习另一个常用的API:Element。元素节点说明element表示模型的元素节点类型,它包含一个拥有名称和子节点的节点类型,继承自Node类。元素属性说明1、name,元素的名称举个例子哈,段落的名称是paragraph,代码块的名称是codeBlock等等。2、childCount, 子元素的数目这里指的是此元素节点包含的子元素的

CKEditor5——模型理解(五:Position, Range, Selection)

今天我们继续学习CK5中模型的一些知识,主要包括:Position, Range, Selection首先,我们需要知道:position表示模型树中的一个位置。模型的位置有两部分组成:root,path。即位置由其根和该根中的路径表示。位置基于偏移量,而不是索引。这意味着两个文本节点 foo 和 bar 之间的位置偏移为 3,而不是 1。由于模型中的位置由位置根和位置路径表示,因此可以创建不存在

CKEditor5——模型理解(六:Range)

上一节我们主要介绍了模型中的Position这个关键的类,今天我们开始学习Range这个类。简单来说的话,如果Position表示一个点的话,那么Range是不是可以理解为一条线段呢?这个线段有一个startPostion,endPosition以及线段的长度等属性,我们暂且这么认为,那么我们可以看看Range官方的文档。从文档中看到,Range类有五个属性:Range属性start:Positi

CKEditor5——模型理解(七:Selection)

昨天我们学习了Range的一些API使用,今天我们看看另一个重要的类Selection的API:Selection的作用是记录鼠标在文档上的选择区域,如果是单个用户在编辑一份文档的时候,选择应该就是一个Range,如果是多个用户在编辑一份文档的时候,那么选择的区域就应该是多个range。因此,我大胆的猜测,Selection中应该有Range数组。我们来看看吧。Selection属性anchor