- Remove child lifecycle event
- Manage local update
- Manage classes
-
-
Save mauroreisvieira/f098b0833c20450735735d2b664b25b3 to your computer and use it in GitHub Desktop.
Virtual DOM lite implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @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(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment