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
属性的作用我们暂时不用理解,后面介绍apply
和revert
方法的时候才来学习这个属性。如果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
的实现代码,我们可以看出它有两个关键方法activateAttributeListener
和getValue
方法,同时这个类有两个继承类,TemplateToBinding
对应的实际上就是bind.to()
,而TemplateIfBinding
对应的就是bind.if()
我们这里需要先看看构造函数:
构造函数里实际上包含多个属性:observable
,emitter
,attribute
,callback
,在执行activateAttributeListener
的时候,实际上就是监听observable
的属性的变化,如果属性有变化,那么这个变化就会同步到dom节点上去。并且返回一个函数,这个函数就是为了取消这种监听。前面我们提到了返回的是一个函数数组,在那里的函数实际上就是这里的返回函数,作用就是revert的时候取消这样的监听。
同时这个类有一个getValue
方法,这个方法就是首先获得observable
的属性值,如果有回调函数,那么就调用回调函数,否则直接返回属性的值。
{
text: bind.to( 'b', ( value, node ) => value.toUpperCase() )
}
//这里就是有对调函数的情况,获取的值是通过对调函数来实现的。
好了,今天我们讲解了render()
方法中的_renderText()
的具体实现,并且简单介绍了绑定是怎么实现的。
另外一个就是_renderElement
方法的实现,我打算下一节来继续分析。欢迎讨论。