v0.0.13: Refactored the render-loop to solve hard-to-track bugs and added more (elaborate) unit-tests to make sure it all works

This commit is contained in:
2020-04-15 16:33:32 +02:00
parent b95e5506d2
commit 0da07549e7
28 changed files with 1113 additions and 235 deletions

View File

@@ -1,5 +1,4 @@
# Don't publish the src containing ESNext proposal's code. Only publish the bundled output in dist/ and the ES6-transpiled src from lib/
src/*
node_modules/*
rollup.config.js
yarn.lock

View File

@@ -110,7 +110,7 @@ export class CsxConfig {
let srcOpts = {
es: format==='es',
single: type==='lib',
sourcemap: true,// Just always there for now
sourcemap: minified ? true : (format==='es'? false : true),
minified: !!minified
};
let outDir = type==='lib'? lib : dist;

View File

@@ -1,6 +1,6 @@
{
"name": "@cerxes/csx",
"version": "0.0.11",
"version": "0.0.13",
"author": "Miel Truyen <miel.truyen@cerxes.net>",
"description": "CSX is a minimalistic UI-framework inspired by React+JSX for usage with WebComponents.",
"repository": {

View File

@@ -0,0 +1,58 @@
import './types';
import {
HostNodeRenderer, Host,
ShadowNodeRenderer, ShadowDOM,
PrimitiveRenderer, Primitive,
NodeTreeRenderer, NativeRenderer
} from "./renderers";
/**
* Meta data used to handle a node in the render lope
* @typedef {VNodeRendererMeta} VNodeMeta
* @category VDOM.renderer
* @property {VNodeRenderer} renderer - The renderer to use to update, create or delete this node
* @property {VNodeType} normedType - Normed type of the node
**/
/**
*
* @param vnode
* @returns {VNodeMeta|undefined}
*/
export function getNodeMeta(vnode) {
if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
if (vnode instanceof Node){
return {
...NativeRenderer.meta(),
renderer: NativeRenderer,
normedType: Node
};
}
let type = vnode?.type;
if (!type){
return {
...PrimitiveRenderer.meta(),
renderer: PrimitiveRenderer,
normedType: Primitive
};
} else if (type === Host){
return {
...HostNodeRenderer.meta(),
renderer: HostNodeRenderer,
normedType: Host
};
} else if (type === ShadowDOM){
return {
...ShadowNodeRenderer.meta(),
renderer: ShadowNodeRenderer,
normedType: ShadowDOM
};
} else {
return {
...NodeTreeRenderer.meta(),
renderer: NodeTreeRenderer,
normedType: window.customElements?.get(type) ?? type
};
}
}

View File

@@ -1,71 +1,53 @@
import './types';
import {
HostNodeRenderer, Host,
ShadowNodeRenderer, ShadowDOM,
PrimitiveRenderer, Primitive,
NodeTreeRenderer, NativeRenderer
} from "./renderers";
export function getNodeMeta(vnode) {
if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
if (vnode instanceof Node) return { renderer: NativeRenderer, normedType: Node };
let type = vnode?.type;
if (!type) return { renderer: PrimitiveRenderer, normedType: Primitive };
else if (type === Host) return { renderer: HostNodeRenderer, normedType: Host };
else if (type === ShadowDOM) return { renderer: ShadowNodeRenderer, normedType: ShadowDOM };
else return { renderer: NodeTreeRenderer, normedType: window.customElements?.get(type) ?? type };
}
/**
* @typedef {Object} RenderOptions
* @category VDOM
* @property {Element} [host] - The element to update to the specified VDOM
* @property {VNode} [old] - Old VNode representation of rendered host
* @property {Document} [document] - The document we're rendering to
* @property {Element} [parent] - The parent element (TODO not sure what this will do when specified; Insert it as child element of the parent where?)
*/
import { getNodeMeta } from "./node-meta";
/**
* This exists as a very basic example/test for JSX-to-DOM
* @category VDOM
* @param {VNode} vnode
* @param {RenderOptions} opts
* @param {RenderOptions} [opts]
* @param {Element} [opts.host] - The element to update to the specified VDOM
* @param {VNode} [opts.old] - Old VNode representation of rendered host
* @param {Document} [opts.document] - The document we're rendering to
* @return {Element}
*/
export function render(vnode, opts = {}) {
// TODO this code could use restructuring when opts.host and vnode.type are incompatible (non updatable type), the host element should be replaced
// with a newly created element, like it does with all child-elements..
// General flow of this code is to process the hierarchy using a queue (so no recursion is used)
// on each node of the hierarchy a renderer is determined which is compared to the renderer of the previous version of this vnode-hierarchy
// to determine if these nodes can be updated (e.g updating a div, or textnode) and if they behave as a child-node (e.g. shows up in childNodes)
// or are some other special type of node (like Host or ShadowDOM)
/**
*
* @type {VRenderState}
*/
/** @type {VRenderState} */
let state = {
keyedElements: new Map(),
refs: [],
queue: [{
// Start item
item: {
document: opts.document || document,
host: opts.host,
parent: opts.parent,
old: opts.old,
vnode: vnode
},
meta: getNodeMeta(vnode)
meta: getNodeMeta(vnode),
parent: null,
attached: true// Root item is treated as if it is attached
}]
};
let newRoot = undefined;
while (state.queue.length > 0) {
let { item, meta, previous } = state.queue.splice(0, 1)[ 0 ];
/** @type {VRenderQueueItem} */
let queueItem = state.queue.shift();
let {
/** @type {VRenderItem} */ item,
/** @type {VNodeMeta} */ meta,
/** @type {VRenderItem} */ previousNode,
/** @type {boolean} */ attached
} = queueItem;
/** @type {VNodeRenderer} */
let renderer = meta.renderer;
if (!renderer) throw new Error("No renderer for vnode", item.vnode);
// SVG handling..
// SVG handling.. (this could be improved)
if (!item.inSvg && item.vnode?.type === 'svg') item.inSvg = true;
else if (item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
@@ -74,8 +56,9 @@ export function render(vnode, opts = {}) {
if (!item.host) {
item.host = renderer.create(item, meta);
newlyCreated = true;
if (item.vnode?.props?.ref) {// If props specify a ref-function, queue it to be called at the end of the render
// If props specify a ref-function, queue it to be called at the end of the render
if (item.vnode?.props?.ref) {
state.refs.push([item.vnode.props.ref, item.host]);
}
}
@@ -84,180 +67,166 @@ export function render(vnode, opts = {}) {
renderer.update(item, meta);
// Update children
if (meta.normedType !== Node && (item.vnode?.children || item.old?.children)) {
let childTypes = new Set();
if (meta.hasChildren) {
/** @type {FlattenedVNode[]} */
let vChildren = flattenVNodeChildren(item.vnode);
/** @type {FlattenedOldVNode[]} */
let oldVChildren = flattenVNodeChildren(item?.old);
/** @type {Map<*, FlattenedOldVNode>} */
let keyedChildren = new Map();// Old child-items
/** @type {NodeState[]} */
let nodes = Array.from(item.host.childNodes).map(node=>({node, attached: true}));
/** @type {FlattenedOldVNode[]} */
let specialOldVChildren = []; // List of old-vnodes that are not node-type node and have no key
// Flatten and organize new vNode-children (this could be a separate function, or implemented using a helper function (because mucht of the code is similar between old/new vnodes)
/**
* @type { Object.<VNodeType, Array.<VRenderQueueItem>> }
*/
let vChildren = {};
let queue = (item.vnode?.children || []).slice();
while (queue.length > 0) {
let next = queue.splice(0, 1)[ 0 ];
if (next instanceof Array) queue.splice(0, 0, ...next);
else {
let meta = getNodeMeta(next);
if (meta && meta.renderer) {
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
let childType = meta.normedType;
if (!meta.renderer.remove) childType = 'node'; // Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
childTypes.add(childType);// Track that children of this type exist and should be iterated later
vChildren[ childType ] = vChildren[ childType ] || []; // Make sure the array exists
vChildren[ childType ].push({
item: {
...item,
old: undefined,
vnode: next,
host: undefined,
parent: item.host
},
meta: meta
});
}
// Iterate old children and link them to their corresponding elements
let nodeI = 0, nodesLength = nodes.length;
let oldVItemI = 0, oldVItemLength = oldVChildren.length;
while(oldVItemI < oldVItemLength){
/** @type {FlattenedOldVNode} */
let oldVItem = oldVChildren[oldVItemI];
/** @type {NodeState} */
let node = nodes[nodeI];
let nodeKey = oldVItem.vnode?.props?.key
if(nodeKey !== undefined){
keyedChildren.set(nodeKey, oldVItem);
}
++oldVItemI;
if(oldVItem.meta.isNode && node){
oldVItem.node = node;
node.old = oldVItem;
++nodeI;
}else if(nodeKey === undefined){
specialOldVChildren.push(oldVItem);
}
}
// Any items not part of the old-vnode spec should be left alone, and ignored in further processing
nodesLength = nodeI; // TODO this feels dirty, but not having this here will screw up our examples, we have yet to add a proper test
// Flatten and organize old-children
/**
* @type { Object.<VNodeType, Array.<VOldQueueItem>> }
*/
let oldVChildren = {};
let curElement = item.host.firstChild;
queue = (item.old?.children || []).slice();
while (queue.length > 0) {
let next = queue.splice(0, 1)[ 0 ];
if (next instanceof Array) queue.splice(0, 0, ...next);
else {
let meta = getNodeMeta(next);
if (meta && meta.renderer) {
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
let childType = meta.normedType;
let childElement;
if (!meta.renderer.remove) {
childType = 'node';// Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
if (curElement) {
childElement = curElement;
curElement = curElement.nextSibling;
}
}
childTypes.add(childType);// Track that children of this type exist and should be iterated later
oldVChildren[ childType ] = oldVChildren[ childType ] || []; // Make sure the array exists
let oldItem = {
vnode: next,
element: childElement,
meta: meta
};
oldVChildren[ childType ].push(oldItem);
if (next.props?.key) {
state.keyedElements.set(next.props?.key, oldItem);
}
}
}
}
let sortedChildTypes = Array.from(childTypes).sort((a, b) => a === 'node' ? 1 : -1); // Always do ChildNode-types last
let queuedItems = [];
/**@type {VRenderQueueItem}*/ let previous = null;
for (let childType of sortedChildTypes) {
let newChildren = vChildren[ childType ];
let oldChildren = oldVChildren[ childType ];
// Iterate new children, remove old nodes as needed and create new queue items
/** @type {VRenderQueueItem[]} */
let queueChildren = [];
/** @type {VRenderItem} */
let previousNodeItem = undefined;
let vItemI = 0, vItemLength = vChildren.length;
nodeI = 0;
while(vItemI < vItemLength || nodeI < nodesLength){
let vItem = vChildren[vItemI];
let node = nodes[nodeI];
while (newChildren && newChildren.length) {
let child = newChildren.splice(0, 1)[ 0 ];
if(vItem){
let nodeKey = vItem?.vnode?.props?.key;
/** @type {FlattenedOldVNode} */
let oldItem = undefined;
if(nodeKey!==undefined){
oldItem = keyedChildren.get(nodeKey);
}
// Key handling
let childKey = child.item.vnode.props?.key;
/**@type {VOldQueueItem}*/ let oldChild;
if (childKey) {
oldChild = state.keyedElements.get(childKey);
if (oldChild) {
if (oldChildren && oldChildren[ 0 ] === oldChild) {
// Old keyed child already in the right place (just clear it from the queue);
oldChildren.splice(0, 1);
/** @type {NodeState} */
let matchedNode = undefined;
// If this is a DOM-node-type match it to an existing node
if(vItem.meta.isNode) {
// Match new item to existing node when possible
matchedNode = oldItem?.node;
if (matchedNode) {
// DOM-node matched by key
if (matchedNode === node) {
// Item in order with old representation
++nodeI;
} else {
// Old keyed child not already in the right place
let indexOfKeyed = oldChildren.indexOf(oldChild);
if (indexOfKeyed) {
oldChildren.splice(indexOfKeyed, 1);
if(oldChild.element) {
item.host.removeChild(oldChild.element);
}else{
// This apparantly happens
}
}
if (previous) {
previous.host.after(oldChild.element);
} else {
//item.parent.prepend(oldChild.element);
item.host.prepend(oldChild.element);
// Out of order, detach
if (matchedNode.attached) {
item.host.removeChild(matchedNode.node);
matchedNode.attached = false;
}
}
}
}
if (!oldChild) oldChild = oldChildren && oldChildren.splice(0, 1)[ 0 ];
child.previous = previous;
if (oldChild && child.meta.normedType === oldChild.meta.normedType && childKey === oldChild.vnode.props?.key
&& (child.meta.normedType !== Node || child.item.vnode === oldChild.vnode)) {
// Update old-child
child.item.host = oldChild.element;
child.item.old = oldChild.vnode;
queuedItems.push(child);
} else {
// New child
if (oldChild) {
if(oldChild.element) {
if (oldChild.meta.renderer.remove){
oldChild.meta.renderer.remove({
...item,
parent: item.host,
host: oldChild.element
});
}else {
item.host.removeChild(oldChild.element);
} else if (node) {
// No match by key
// If old VNode associated, check if updatable
if (node.old
&& node.old.vnode?.props?.key === undefined
&& node.old.meta.normedType === vItem.meta.normedType
) {
oldItem = node.old;
matchedNode = node;
++nodeI;
} else {
// Not updatable, detach
if (node.attached) {
item.host.removeChild(node.node);
node.attached = false;
}
}else{
// This apparantly happens
}
}
queuedItems.push(child);
}
if (!child.meta.renderer.remove) {
// If child is a node-type item track it as the previous (so we can insert next node-type items after it as intended)
previous = child.item;
}
}
while (oldChildren && oldChildren.length) {
let oldChild = oldChildren.splice(0, 1)[ 0 ];
if(oldChild.element) {
if (oldChild.meta.renderer.remove) {
oldChild.meta.renderer.remove({ ...item, parent: item.host, host: oldChild.element });
} else {
item.host.removeChild(oldChild.element);
}
}else{
// This apparantly happens
// If this is a special type and it was not matched to an old VNode by key, find it the first non-keyed
// old VNode with the same normedType (These are ShadowDOM or Host-types)
if(!oldItem){
let indexOf = specialOldVChildren.findIndex(old=>old.meta.normedType===vItem.meta.normedType);
if(indexOf>=0){
oldItem = specialOldVChildren[indexOf];
specialOldVChildren.splice(indexOf,1);
}
}
}
// Track that the node is used by a new node (we don't use this anywhere?!)
if(matchedNode){
matchedNode.new = vItem;
}
/** @type {VRenderQueueItem} */
let queueItem = {
item: {
vnode: vItem.vnode,
document: item.document,
host: matchedNode?.node,
old: oldItem?.vnode,
inSvg: item.inSvg,
parent: item,
},
attached: matchedNode?.attached,
previousNode: previousNodeItem,
meta: vItem.meta
}
queueChildren.push(queueItem);
if(vItem.meta.isNode){
previousNodeItem = queueItem.item;
}
++vItemI;
}else if(node){
// Trailing node item
if(node.attached && !node.new) {
// If this node was not reused for a new vnode representation, and was not already detached
// for other reasons, remove it
item.host.removeChild(node.node);
node.attached = false;
}
++nodeI;
}
}
state.queue.splice(0, 0, ...queuedItems);
state.queue.unshift(...queueChildren);
// Remove any special types (Host, ShadowDOM that were no longer present)
for(let oldSpecialVnode of specialOldVChildren){
oldSpecialVnode.meta.renderer.remove?.(oldSpecialVnode.vnode);
}
}
if (newlyCreated) {
if (!meta.renderer.remove) {
if (item.parent) {
if (!previous) {
// First child
item.parent.prepend(item.host);
} else {
// Subsequent child
previous.host.after(item.host);
}
}
if (!attached && meta.isNode) {
if (previousNode) {
// Subsequent child
previousNode.host.after(item.host);
} else if (item.parent) {
// First child
item.parent.host.prepend(item.host);
}
if (!item.parent) newRoot = item.host;
}
if(!item.parent && !newRoot){
newRoot = item.host;
}
}
@@ -266,3 +235,46 @@ export function render(vnode, opts = {}) {
}
return newRoot;
}
/**
* Intermediate type used to process child-items
* @typedef {object} FlattenedVNode
* @property {VNode} vnode
* @property {VNodeMeta} meta
*/
/**
* @typedef {object} NodeState
* @property {ChildNode} node
* @property {boolean} attached
* @property {FlattenedOldVNode} old
* @property {FlattenedVNode} new
*/
/**
* Intermediate type used to process old child-items
* @typedef {FlattenedVNode} FlattenedOldVNode
* @property {NodeState} node - The DOM-node
*/
/**
*
* @param {VNode} vnode
* @returns {FlattenedVNode[]}
*/
function flattenVNodeChildren(vnode){
let children = [];
let queue = (vnode?.children || []).slice();
while (queue.length > 0) {
let next = queue.shift();
if (next instanceof Array) queue.splice(0, 0, ...next);
else {
let meta = getNodeMeta(next);
if (meta) {
children.push({ vnode: next, meta });
}else{
// null or undefined child, ignored
}
}
}
return children;
}

View File

@@ -10,12 +10,22 @@ export const Host = Symbol('Host');
* @implements {VNodeRenderer}
*/
export const HostNodeRenderer = {
/**
* @return {VNodeRendererMeta}
*/
meta(){
return {
hasChildren: true,
isNode: true, // Host node should always
};
},
/**
* @param {VRenderItem} item
*/
create(item){
if(!item.parent) throw new Error("Host node cannot appear as a top-level element unless a parent is provided");
else return item.parent;
if(!item.parent.host) throw new Error("Host node cannot appear as a top-level element unless a parent is provided");
else return item.parent.host;
},
/**
@@ -31,7 +41,7 @@ export const HostNodeRenderer = {
* @param {VRenderState} state
*/
update(item, state){
item.host = item.host || item.parent;
item.host = item.host || item.parent.host;
NodeTreeRenderer.update(item,state);
},
};

View File

@@ -7,6 +7,16 @@ import '../types';
* @implements {VNodeRenderer}
*/
export const NativeRenderer = {
/**
* @return {VNodeRendererMeta}
*/
meta(){
return {
hasChildren: false,// Counter intiutitive, but this is false as CSX will never manage children of a premade DOM-element
isNode: true,
};
},
/**
* @param {VRenderItem} item
*/
@@ -18,6 +28,9 @@ export const NativeRenderer = {
* @param {VRenderItem} item
*/
update(item){
return;// NO-OP
if(item.old && item.old!==item.vnode){
item.host.replaceWith(item.vnode);
}
return;
}
};

View File

@@ -9,6 +9,16 @@ export const Primitive = Symbol("primitive");
* @implements {VNodeRenderer}
*/
export const PrimitiveRenderer = {
/**
* @return {VNodeRendererMeta}
*/
meta(){
return {
hasChildren: false,// Primitive type (TextNode) can't have children
isNode: true,
};
},
/**
* @param {VRenderItem} item
*/

View File

@@ -12,6 +12,16 @@ let namespace = {
* @implements {VNodeRenderer}
*/
export const NodeTreeRenderer = {
/**
* @return {VNodeRendererMeta}
*/
meta(){
return {
hasChildren: true,
isNode: true,
};
},
/**
*
* @param {VRenderItem} item

View File

@@ -9,12 +9,22 @@ export const ShadowDOM = Symbol('ShadowDOM');
* @implements {VNodeRenderer}
*/
export const ShadowNodeRenderer = {
/**
* @return {VNodeRendererMeta}
*/
meta(){
return {
hasChildren: true,
isNode: false,
};
},
/**
* @param {VRenderItem} item
*/
create(item) {
if (!item.parent) throw new Error("ShadowDOM node cannot appear as a top-level element unless a parent is provided");
else return item.parent.shadowRoot || item.parent.attachShadow({ mode: 'open' });// TODO Pass props as options? (e.g. delegateFocus, mode)
else return item.parent.host.shadowRoot || item.parent.host.attachShadow({ mode: 'open' });// TODO Pass props as options? (e.g. delegateFocus, mode)
},
/**
@@ -28,6 +38,6 @@ export const ShadowNodeRenderer = {
* @param {VRenderItem} item
*/
update(item) {
item.host = item.host || item.parent.shadowRoot;
item.host = item.host || item.parent.host.shadowRoot;
},
};

View File

@@ -2,3 +2,4 @@ export * from "./vnode";
export * from "./render-item";
export * from "./render-state";
export * from "./vnode-renderer";
export * from "./renderer-meta";

View File

@@ -2,13 +2,12 @@ import './vnode';
/**
* Per node rendering-state when rendering a tree of VNodes
* @typedef VRenderItem
* @interface
* @interface VRenderItem
* @category VDOM.renderer
* @property {VNode} vnode - The VNode representation to update to
* @property {VNode} [old] - The previous VNode representation of this item
* @property {Element} host - The DOM-node being rendered
* @property {Document} document - The DOM-document to be added to
* @property {boolean} inSvg - Indicates whether this node is a child of an SVG element, and should thus be created with createElementNS(...)
* @property {Element} [parent] - Parent DOM-node
* @property {VRenderItem} [parent] - Parent render item
**/

View File

@@ -6,8 +6,7 @@ import "./vnode";
/**
* Per node rendering-state when rendering a tree of VNodes
* @typedef VRenderQueueItemMetadata
* @interface
* @typedef {object} VRenderQueueItemMetadata
* @category VDOM.renderer
* @property {VNodeRenderer} renderer - The renderer that will render this item
* @property {VNodeType} normedType - The normed type of a VNode, for most VNode this just maps to vnode.type, but a text-node normally does not have a type.
@@ -16,30 +15,28 @@ import "./vnode";
/**
* Per node rendering-state when rendering a tree of VNodes
* @typedef VRenderQueueItem
* @interface
* @typedef {object} VRenderQueueItem
* @category VDOM.renderer
* @property {VRenderItem} item - The item to queue for rendering
* @property {VRenderItem} item - The item queued for rendering
* @property {VRenderQueueItemMetadata} meta - Meta data for the item such as normedType and the renderer to use(from a preprocessing stage)
* @property {VRenderItem} previous - The item that will have been inserted before this one
* @property {VRenderItem} [previousNode] - The previous node-item that will have been inserted before this one
* @property {boolean} [attached] - Whether the host-node was already attached to the dom
**/
/**
* Temporary data structure for listing an old VNode
* @typedef VOldQueueItem
* @interface
* @typedef {object} VOldQueueItem
* @category VDOM.renderer
* @property {VNode} vnode - The old vnode
* @property {VRenderQueueItemMetadata} meta - Meta data for the item such as normedType and the renderer to use(from a preprocessing stage)
* @property {Element} element - The matching element
* @property {boolean} detached - Indicates if the element was already removed
**/
/**
* Global rendering-state when rendering a tree of VNodes
* @typedef VRenderState
* @interface
* @typedef {object} VRenderState
* @category VDOM.renderer
* @property {Array.<VRenderQueueItem>} queue - The queue of items to be rendered
* @property {Array.<[Function,Element]>} refs - Ref-callback functions be called when rendering is done
* @property {Map.<string, VOldQueueItem>} keyedElements - A map of (old) keyed elements
**/

View File

@@ -0,0 +1,9 @@
import './vnode';
/**
* Per node rendering-state when rendering a tree of VNodes
* @interface VNodeRendererMeta
* @category VDOM.renderer
* @property {boolean} isNode - Whether the node-type handled by this renderer has a place in the DOM hierarchy
* @property {boolean} hasChildren - Indicates if the node-type handled by this renderer can have child-nodes
**/

View File

@@ -1,18 +1,26 @@
import "./render-item";// Info about what we're rendering and where to
import "./renderer-meta"; // Meta-data about the type of node handled
// Note: This type is not meant to be public
/**
* Represents a renderer capable of rendering a VNode of a certain type
* @interface VNodeRenderer
* @class
**/
/**
* This method creates the element corresponding to a vnode
* @method
* @name VNodeRenderer#meta
* @returns {VNodeRendererMeta}
*/
/**
* This method creates the element corresponding to a vnode
* @method
* @name VNodeRenderer#create
* @param {VRenderItem} item
* @param {VRenderItem} parent
* @returns {Element}
*/

View File

@@ -18,8 +18,7 @@
/**
* A tree of virtual-nodes (e.g, type,props,attr and nested children)
* @typedef VNodeTree
* @interface
* @interface VNodeTree
* @category VDOM
* @property {VNodeType} type - TagName or CustomElement of the html-element
* @property {VNodeProps} props - Properties to set on the element