CKEditor5——Template理解(三)

CKEditor5——Template理解(三)

上一节我们分析了render()方法中关键的_renderText(),我们再来看看另一个方法就是renderElement()

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

	if ( !node ) {
		node = data.node = document.createElementNS( this.ns || xhtmlNs, this.tag );
	}

	this._renderAttributes( data );
	this._renderElementChildren( data );
	this._setUpListeners( data );

	return node;
}

从这里可以看出,渲染元素节点和文本节点有差别,那就是渲染元素节点,分为三步:

1、渲染节点的属性

2、渲染元素的子元素

3、设置当前元素的事件监听器。

在此之前需要判断当前数据是否存在node属性,一般来说,如果是render()方法调用,那么是没有node属性的,需要根据tag来创建一个节点,然后才开始渲染;而apply方法是直接有node属性,可以直接渲染。

我们再来看看this._renderAttributes( data )

_renderAttributes( data ) {
	let attrName, attrValue, domAttrValue, attrNs;

	if ( !this.attributes ) {
		return;
	}

	const node = data.node;
	const revertData = data.revertData;

	for ( attrName in this.attributes ) {
		// Current attribute value in DOM.
		domAttrValue = node.getAttribute( attrName );

		// The value to be set.
		attrValue = this.attributes[ attrName ];

		// Save revert data.
		if ( revertData ) {
			revertData.attributes[ attrName ] = domAttrValue;
		}

		// Detect custom namespace:
		//
		//		class: {
		//			ns: 'abc',
		//			value: Template.bind( ... ).to( ... )
		//		}
		//
		attrNs = ( isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ) ? attrValue[ 0 ].ns : null;

		// Activate binding if one is found. Cases:
		//
		//		class: [
		//			Template.bind( ... ).to( ... )
		//		]
		//
		//		class: [
		//			'bar',
		//			Template.bind( ... ).to( ... ),
		//			'baz'
		//		]
		//
		//		class: {
		//			ns: 'abc',
		//			value: Template.bind( ... ).to( ... )
		//		}
		//
		if ( hasTemplateBinding( attrValue ) ) {
			// Normalize attributes with additional data like namespace:
			//
			//		class: {
			//			ns: 'abc',
			//			value: [ ... ]
			//		}
			//
			const valueToBind = attrNs ? attrValue[ 0 ].value : attrValue;

			// Extend the original value of attributes like "style" and "class",
			// don't override them.
			if ( revertData && shouldExtend( attrName ) ) {
				valueToBind.unshift( domAttrValue );
			}

			this._bindToObservable( {
				schema: valueToBind,
				updater: getAttributeUpdater( node, attrName, attrNs ),
				data
			} );
		}

		// Style attribute could be an Object so it needs to be parsed in a specific way.
		//
		//		style: {
		//			width: '100px',
		//			height: Template.bind( ... ).to( ... )
		//		}
		//
		else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
			this._renderStyleAttribute( attrValue[ 0 ], data );
		}

		// Otherwise simply set the static attribute:
		//
		//		class: [ 'foo' ]
		//
		//		class: [ 'all', 'are', 'static' ]
		//
		//		class: [
		//			{
		//				ns: 'abc',
		//				value: [ 'foo' ]
		//			}
		//		]
		//
		else {
			// Extend the original value of attributes like "style" and "class",
			// don't override them.
			if ( revertData && domAttrValue && shouldExtend( attrName ) ) {
				attrValue.unshift( domAttrValue );
			}

			attrValue = attrValue
				// Retrieve "values" from:
				//
				//		class: [
				//			{
				//				ns: 'abc',
				//				value: [ ... ]
				//			}
				//		]
				//
				.map( val => val ? ( val.value || val ) : val )
				// Flatten the array.
				.reduce( ( prev, next ) => prev.concat( next ), [] )
				// Convert into string.
				.reduce( arrayValueReducer, '' );

			if ( !isFalsy( attrValue ) ) {
				node.setAttributeNS( attrNs, attrName, attrValue );
			}
		}
	}
}

这个方法的逻辑比较多,但是总的来说分为两种情况,对普通属性的处理和对有bind.to()或者bind.if()

我们具体来看看:

1、首先是将当前节点的属性值取出来进行迭代,如果不存在直接返回,同时拿出当前数据的revertData数据用于记录之前的状态。

2、其次在迭代中判断属性是否为TemplateBinding,如果是的话需要设置响应的Observable监听,这里的监听我们在上一节已经分析过,感兴趣的可以去看看。

3、如果属性的名称是style,也就是css的style属性,那么需要调用单独的this._renderStyleAttribute

这个方法的处理和属性处理类似

4、最后一种情况就是普通属性的处理,因为属性的值一般都是数组,因此将这些属性联结起来后设置到节点上即可。

 

我们再看看第二方法this._renderElementChildren( data )

_renderElementChildren( data ) {
	const node = data.node;
	const container = data.intoFragment ? document.createDocumentFragment() : node;
	const isApplying = data.isApplying;
	let childIndex = 0;

	for ( const child of this.children ) {
		if ( isViewCollection( child ) ) {
			if ( !isApplying ) {
				child.setParent( node );

				// Note: ViewCollection renders its children.
				for ( const view of child ) {
					container.appendChild( view.element );
				}
			}
		} else if ( isView( child ) ) {
			if ( !isApplying ) {
				if ( !child.isRendered ) {
					child.render();
				}

				container.appendChild( child.element );
			}
		} else if ( isNode( child ) ) {
			container.appendChild( child );
		} else {
			if ( isApplying ) {
				const revertData = data.revertData;
				const childRevertData = getEmptyRevertData();

				revertData.children.push( childRevertData );

				child._renderNode( {
					node: container.childNodes[ childIndex++ ],
					isApplying: true,
					revertData: childRevertData
				} );
			} else {
				container.appendChild( child.render() );
			}
		}
	}

	if ( data.intoFragment ) {
		node.appendChild( container );
	}
}

这里渲染子节点的的时候也分为两种情况,render调用和apply调用:

1、如果是render()调用,那么此时会data.intoFragment为true,根据逻辑,会创建一个documentFragment

将这个节点作为container,然后分为三种情况处理节点:第一种情况是如果子节点是ViewCollection,此时就直接将节点的element添加到container;第二种情况是如果子节点是View,那么需要渲染后才能添加子节点的element;最后一种情况是节点是一个Node,此时直接添加就可以啦;

2、如果是apply()调用,那么这里实际上就是一种递归调用,然后将渲染后的子节点添加到container

具体的逻辑大家可以参考源代码进行分析,如果有啥问题,欢迎讨论哈。

最后我们看看这个方法this._setUpListeners( data )

_setUpListeners( data ) {
	if ( !this.eventListeners ) {
		return;
	}

	for ( const key in this.eventListeners ) {
		const revertBindings = this.eventListeners[ key ].map( schemaItem => {
			const [ domEvtName, domSelector ] = key.split( '@' );

			return schemaItem.activateDomEventListener( domEvtName, domSelector, data );
		} );

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

如果大家看看上一篇的TemplateBinding的分析,就会对这里有所理解,这个方法的主要作用就是看看当前节点是否存在eventListeners,如果不存在直接返回,如果存在的话,那么迭代此监听器,这个迭代的值

this.eventListeners[key]实际上就是TemplateBinding类型,这里的逻辑就是从key中获取事件名称和dom元素选择器,调用activateDomEventListener绑定dom节点事件,返回值也是一个函数,这个函数的作用就是取消绑定,从这里不难看出revertBindings是一个取消绑定的函数,而这个值会被添加到data.revertData.bindings这个数组中,在revert()方法调用恢复以前的状态的时候会取消dom事件的绑定。

好了,这个渲染dom的逻辑基本上就分析结束了。

这里提一个问题,在渲染子节点的时候,如果child是ViewCollection的时候,为什么子节点不需要渲染呢?

这里我们之前分析的时候其实已经理解过,欢迎大家讨论学习。

 

 

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

相关推荐

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树结构。我们知道

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

RxJs——Subscription理解

我們前段時間學習了Observable相關的知識,今天我們學習另一個重要的概念:Subscription。首先,我們看看Subscription官方文檔的介紹如下:What is a Subscription? A Subscription is an object that represents a disposable resource, usually the execution of an