Skip to content

Instantly share code, notes, and snippets.

@LucaColonnello
Last active April 14, 2020 09:57
Show Gist options
  • Save LucaColonnello/aa69351ce36d86f1d6185b4eba51126a to your computer and use it in GitHub Desktop.
Save LucaColonnello/aa69351ce36d86f1d6185b4eba51126a to your computer and use it in GitHub Desktop.
Virtual DOM lite implementation
/** @jsx h */
// refs
// https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060
// https://medium.com/@deathmood/write-your-virtual-dom-2-props-events-a957608f5c76
// test: https://jsfiddle.net/Luca_Colonnello/Lzjy5a67/1/
// comparison:
// current - https://jsfiddle.net/Luca_Colonnello/5xebu97u/3/
// vidom - https://jsfiddle.net/Luca_Colonnello/dp9cwn37/2/
// react - https://jsfiddle.net/Luca_Colonnello/o9cdrfs2/1/
function h(type, props, ...children) {
if (typeof type === 'function') {
const component = type({
...props,
children
});
component.isComponentRoot = true;
component.componentProps = props;
return component;
}
return { isNode: true, type, props: props || {}, children };
}
function map(list, func) {
return { isMap: true, list, func };
}
function isMap(element) {
return typeof element === 'object' && typeof element.isMap !== 'undefined';
}
function setBooleanProp($target, name, value) {
if (value) {
$target.setAttribute(name, value);
$target[name] = true;
} else {
$target[name] = false;
}
}
function removeBooleanProp($target, name) {
$target.removeAttribute(name);
$target[name] = false;
}
function isEventProp(name) {
return /^on/.test(name);
}
function extractEventName(name) {
return name.slice(2).toLowerCase();
}
function isCustomProp(name) {
return isEventProp(name) || name === 'forceUpdate';
}
function setProp($target, name, value) {
if (isCustomProp(name)) {
return;
} else if (name === 'className') {
$target.setAttribute('class', value);
} else if (typeof value === 'boolean') {
setBooleanProp($target, name, value);
} else {
$target.setAttribute(name, value);
}
}
function removeProp($target, name, value) {
if (isCustomProp(name)) {
return;
} else if (name === 'className') {
$target.removeAttribute('class');
} else if (typeof value === 'boolean') {
removeBooleanProp($target, name);
} else {
$target.removeAttribute(name);
}
}
function setProps($target, props) {
props && Object.keys(props).forEach(name => {
setProp($target, name, props[name]);
});
}
function updateProp($target, name, newVal, oldVal) {
if (newVal === undefined) {
if (isEventProp(name)) {
$target.removeEventListener(
extractEventName(name),
oldVal
);
} else {
removeProp($target, name, oldVal);
}
} else if (oldVal === undefined || newVal !== oldVal) {
if (isEventProp(name)) {
$target.removeEventListener(
extractEventName(name),
oldVal
);
$target.addEventListener(
extractEventName(name),
newVal
);
} else {
setProp($target, name, newVal);
}
}
}
function updateProps($target, newProps, oldProps = {}) {
const props = Object.assign({}, newProps, oldProps);
Object.keys(props).forEach(name => {
updateProp($target, name, newProps[name], oldProps[name]);
});
}
function addEventListeners($target, props) {
props && Object.keys(props).forEach(name => {
if (isEventProp(name)) {
$target.addEventListener(
extractEventName(name),
props[name]
);
}
});
}
function createElement(node) {
if (typeof node.isNode === 'undefined') {
return document.createTextNode(node.toString());
}
const $el = document.createElement(node.type);
setProps($el, node.props);
addEventListeners($el, node.props);
if (node.props.ref && typeof node.props.ref === 'function') {
node.props.ref($el);
}
const parsedChildren = [];
const loop = (children) => children.forEach(child => {
if (Array.isArray(child)) {
return loop(child);
}
if (isMap(child)) {
child.list.forEach((value, index) => {
const parsed = child.func(value, index);
$el.appendChild(createElement(parsed));
parsedChildren.push(parsed);
});
return;
}
$el.appendChild(createElement(child));
parsedChildren.push(child);
});
loop(node.children);
node.children = parsedChildren;
if (node.isComponentRoot && node.componentProps.onCreated) {
node.componentProps.onCreated($el);
}
if (node.props.onCreated) {
node.props.onCreated($el);
}
return $el;
}
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === 'string' && node1 !== node2 ||
!node1.isNode && node1 !== node2 ||
node1.type !== node2.type ||
node1.props && node1.props.forceUpdate;
}
function updateElement($parent, newNode, oldNode, index = 0) {
if (oldNode === undefined) {
$parent.appendChild(
createElement(newNode)
);
} else if (newNode === undefined) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
if (newNode.isComponentRoot) {
if (
newNode.componentProps.shouldUpdate &&
!newNode.componentProps.shouldUpdate(oldNode.componentProps || newNode.componentProps, newNode.componentProps)
) {
return;
}
if (
newNode.props.shouldUpdate &&
!newNode.props.shouldUpdate(oldNode.componentProps || newNode.componentProps, newNode.componentProps)
) {
return;
}
}
updateProps(
$parent.childNodes[index],
newNode.props || {},
oldNode.props || {}
);
let loopCounter = 0;
const parsedChildren = [];
const parseChild = (newNodeChild, oldNodeChildren) => {
if (Array.isArray(newNodeChild)) {
newNodeChild.forEach((child) => parseChild(child, oldNodeChildren));
return;
}
if (isMap(newNodeChild)) {
newNodeChild.list.forEach((value, _index) => {
const node = newNodeChild.func(value, _index);
parsedChildren.push(node);
updateElement(
$parent.childNodes[index],
node,
oldNodeChildren[loopCounter],
loopCounter
);
loopCounter++;
});
return;
}
parsedChildren.push(newNodeChild);
updateElement(
$parent.childNodes[index],
newNodeChild,
oldNodeChildren[loopCounter],
loopCounter
);
loopCounter++;
};
while (loopCounter < newNode.children.length || loopCounter < oldNode.children.length) {
parseChild(newNode.children[loopCounter], oldNode.children);
}
newNode.children = parsedChildren;
if (newNode.isComponentRoot && newNode.componentProps.onUpdated) {
newNode.componentProps.onUpdated(oldNode.componentProps || newNode.componentProps, newNode.componentProps);
}
if (newNode.props.onUpdated) {
newNode.props.onUpdated(oldNode.props || newNode.props, newNode.props);
}
}
}
//---------------------------------------------------------
let inputText = 'test';
const listItems = Array(5000).fill(false);
let rootNode;
function inputChange(e) {
inputText = e.target.value;
update();
}
function listItemChecked(e) {
const index = parseInt(e.target.getAttribute('dataIndex'), 10);
const checked = e.target.checked;
listItems[index] = checked;
update();
}
const shouldUpdate = (oldProps, newProps) => {
return oldProps.checked != newProps.checked;
};
const ListItem = ({ children, checked, index }) =>
(<li shouldUpdate={shouldUpdate} onUpdated={() => console.log('ListItem updated')}>
<input type="checkbox" checked={checked} dataIndex={index} onClick={listItemChecked} /> <span>{children}</span> - <span>{checked ? 'Selected' : 'Not selected'}</span>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facilis fugit minima voluptatibus voluptatem, dicta deleniti consequatur, odit ut provident corporis, nobis distinctio in perspiciatis similique, sit unde ipsa libero vero!</p>
</li>);
const Component = ({ text }) => (
<ul
style="list-style: none;"
onCreated={($el) => {
console.log('root created', $el);
rootNode = $el;
}}
onUpdated={() => {
console.log('root updated');
}}
>
<li className="item item2" onClick={() => alert(text)}>item 1</li>
<li style="background: red;">
<input type="checkbox" />
<input type="text" onInput={inputChange} value={text} />
</li>
{text.length > 10 && <li>More than 10!</li> || ''}
<li>{text}</li>
{/* faster implementation */}
{map(listItems, (checked, index) => (<ListItem checked={checked} shouldUpdate={shouldUpdate} index={index}>{index}</ListItem>))}
{/* this is so slow... */}
{/*listItems.map((checked, index) => (<ListItem checked={checked} shouldUpdate={shouldUpdate} index={index}>{index}</ListItem>))*/}
</ul>
);
const $root = document.getElementById('root');
let currentNode;
const update = () => {
const node = <Component text={inputText} />;
updateElement($root, node, currentNode);
currentNode = node;
};
update();
  • Remove child lifecycle event
  • Manage local update
  • Manage classes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment