CKEditor5——Node,Element,NodeList源码分析

前面大概知道了模型的基本组成,包括一个抽象类Node
,两个实现类Text
和Element
,以及还有一个关键的类就是NodeList
。
为了容易展开这个话题,我首先提出一个问题,那就是为什么是Node
类是一个抽象类?先看看这个类的具体代码:
export default class Node {
/**
* Creates a model node.
*
* This is an abstract class, so this constructor should not be used directly.
*
* @abstract
* @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
*/
constructor( attrs ) {
/**
* Parent of this node. It could be {@link module:engine/model/element~Element}
* or {@link module:engine/model/documentfragment~DocumentFragment}.
* Equals to `null` if the node has no parent.
*
* @readonly
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
this.parent = null;
/**
* Attributes set on this node.
*
* @private
* @member {Map} module:engine/model/node~Node#_attrs
*/
this._attrs = toMap( attrs );
}
}
我们看这个类,首先,这个类应该能够存储属性,因此它有一个Map属性叫做this._attrs
,此外还有一个属性叫做this.parent
,因此有了这个属性之后,就可以用Node节点构成一个类似树形结构。
有了这个基础知识以后,看看一个具体的方法:
get index() {
let pos;
if ( !this.parent ) {
return null;
}
if ( ( pos = this.parent.getChildIndex( this ) ) === null ) {
throw new CKEditorError( 'model-node-not-found-in-parent', this );
}
return pos;
}
注意到没有,这个类的所有实现都是代理到了它的父节点的方法,在this.parent
这个属性的类型有三类:Element,DocumentFragment,null
,因此,可以大胆猜想,除了null之外,其他的类一定要实现具体方法。
用获取某个节点的索引来举例,实现的逻辑就是,判断当前节点在父节点中的索引位置就是具体的索引值。因此,我们可以看看Element.getChildIndex()
这个方法:
Element类
export default class Element extends Node {
/**
* Creates a model element.
*
* **Note:** Constructor of this class shouldn't be used directly in the code.
* Use the {@link module:engine/model/writer~Writer#createElement} method instead.
*
* @protected
* @param {String} name Element's name.
* @param {Object} [attrs] Element's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
* One or more nodes to be inserted as children of created element.
*/
constructor( name, attrs, children ) {
super( attrs );
/**
* Element name.
*
* @readonly
* @member {String} module:engine/model/element~Element#name
*/
this.name = name;
/**
* List of children nodes.
*
* @private
* @member {module:engine/model/nodelist~NodeList} module:engine/model/element~Element#_children
*/
this._children = new NodeList();
if ( children ) {
this._insertChild( 0, children );
}
}
}
从Element类可以看出,这个类除了能保存属性值之外,还有两个属性就是名称和子节点,而子节点的实现就是NodeList
。
我们继续看getChildIndex()
getChildIndex( node ) {
return this._children.getNodeIndex( node );
}
这个方法的具体实现其实是代理给了NodeList
这个类。
export default class NodeList {
/**
* Creates an empty node list.
*
* @protected
* @param {Iterable.<module:engine/model/node~Node>} nodes Nodes contained in this node list.
*/
constructor( nodes ) {
/**
* Nodes contained in this node list.
*
* @private
* @member {Array.<module:engine/model/node~Node>}
*/
this._nodes = [];
if ( nodes ) {
this._insertNodes( 0, nodes );
}
}
}
而NodeList
类其实就是Node数组的一个封装。
getNodeIndex()
getNodeIndex( node ) {
const index = this._nodes.indexOf( node );
return index == -1 ? null : index;
}
这下逻辑很清晰了吧,获取某个节点在数组中的位置,然后判断,如果不存在的话,返回null,否则返回具体的索引。知道理清楚了Node,NodeList,Element之间的关系,就能明白每一个方法是怎样实现的。
好了,剩余的其它方法都可以按照上面的逻辑来进行梳理和分析。
我们再看看一个属性叫做offsetSize
,这个属性表示当前节点占据的偏移量值的大小,普通节点的值是1,而文本节点的大小就是文本的长度。
Text
get offsetSize() {
return this.data.length;
}
Node
get offsetSize() {
return 1;
}
另一个属性就是maxOffset
这个属性是只有Element,其实表示的就是当前节点占据的偏移量的大小。
get maxOffset() {
return this._children.maxOffset;
}
NodeList
get maxOffset() {
return this._nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}
这里的逻辑就是将子节点的offsetSize相加即可。注意一点的是,这里的偏移量一定是相对于父节点的。