Created
September 18, 2021 02:02
-
-
Save eihigh/72b3d52d2d5594363f8e232e618f4c9b to your computer and use it in GitHub Desktop.
vdom
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
var SSR_NODE = 1, | |
TEXT_NODE = 3, | |
EMPTY_OBJ = {}, | |
EMPTY_ARR = [], | |
SVG_NS = "http://www.w3.org/2000/svg"; | |
var listener = function (event) { | |
this.events[event.type](event); | |
}; | |
var getKey = (vdom) => (vdom == null ? vdom : vdom.key); | |
var patchProperty = (node, key, oldValue, newValue, isSvg) => { | |
if (key === "key") { | |
} else if (key[0] === "o" && key[1] === "n") { | |
if ( | |
!((node.events || (node.events = {}))[(key = key.slice(2))] = newValue) | |
) { | |
node.removeEventListener(key, listener); | |
} else if (!oldValue) { | |
node.addEventListener(key, listener); | |
} | |
} else if (!isSvg && key !== "list" && key !== "form" && key in node) { | |
node[key] = newValue == null ? "" : newValue; | |
} else if (newValue == null || newValue === false) { | |
node.removeAttribute(key); | |
} else { | |
node.setAttribute(key, newValue); | |
} | |
}; | |
var createNode = (vdom, isSvg) => { | |
var props = vdom.props, | |
node = | |
vdom.type === TEXT_NODE | |
? document.createTextNode(vdom.tag) | |
: (isSvg = isSvg || vdom.tag === "svg") | |
? document.createElementNS(SVG_NS, vdom.tag, { is: props.is }) | |
: document.createElement(vdom.tag, { is: props.is }); | |
for (var k in props) { | |
patchProperty(node, k, null, props[k], isSvg); | |
} | |
for (var i = 0; i < vdom.children.length; i++) { | |
node.appendChild( | |
createNode((vdom.children[i] = vdomify(vdom.children[i])), isSvg) | |
); | |
} | |
return (vdom.node = node); | |
}; | |
var fns = []; | |
var patchNode = (parent, node, oldVNode, newVNode, isSvg) => { | |
if (oldVNode === newVNode) { | |
} else if ( | |
oldVNode != null && | |
oldVNode.type === TEXT_NODE && | |
newVNode.type === TEXT_NODE | |
) { | |
if (oldVNode.tag !== newVNode.tag) { | |
fns.push(() => { | |
node.nodeValue = newVNode.tag; | |
}); | |
} | |
} else if (oldVNode == null || oldVNode.tag !== newVNode.tag) { | |
node = parent.insertBefore( | |
createNode((newVNode = vdomify(newVNode)), isSvg), | |
node | |
); | |
if (oldVNode != null) { | |
parent.removeChild(oldVNode.node); | |
} | |
} else { | |
var tmpVKid, | |
oldVKid, | |
oldKey, | |
newKey, | |
oldProps = oldVNode.props, | |
newProps = newVNode.props, | |
oldVKids = oldVNode.children, | |
newVKids = newVNode.children, | |
oldHead = 0, | |
newHead = 0, | |
oldTail = oldVKids.length - 1, | |
newTail = newVKids.length - 1; | |
isSvg = isSvg || newVNode.tag === "svg"; | |
for (var i in { ...oldProps, ...newProps }) { | |
if ( | |
(i === "value" || i === "selected" || i === "checked" | |
? node[i] | |
: oldProps[i]) !== newProps[i] | |
) { | |
patchProperty(node, i, oldProps[i], newProps[i], isSvg); | |
} | |
} | |
while (newHead <= newTail && oldHead <= oldTail) { | |
if ( | |
(oldKey = getKey(oldVKids[oldHead])) == null || | |
oldKey !== getKey(newVKids[newHead]) | |
) { | |
break; | |
} | |
patchNode( | |
node, | |
oldVKids[oldHead].node, | |
oldVKids[oldHead++], | |
(newVKids[newHead] = vdomify(newVKids[newHead++])), | |
isSvg | |
); | |
} | |
while (newHead <= newTail && oldHead <= oldTail) { | |
if ( | |
(oldKey = getKey(oldVKids[oldTail])) == null || | |
oldKey !== getKey(newVKids[newTail]) | |
) { | |
break; | |
} | |
patchNode( | |
node, | |
oldVKids[oldTail].node, | |
oldVKids[oldTail--], | |
(newVKids[newTail] = vdomify(newVKids[newTail--])), | |
isSvg | |
); | |
} | |
if (oldHead > oldTail) { | |
// oldでkeyをすべて見終わったら、残ったnewを追加する。完了 | |
while (newHead <= newTail) { | |
node.insertBefore( | |
createNode((newVKids[newHead] = vdomify(newVKids[newHead++])), isSvg), | |
(oldVKid = oldVKids[oldHead]) && oldVKid.node | |
); | |
} | |
} else if (newHead > newTail) { | |
// newでkeyをすべて見終わったら、残ったoldを削除する。完了 | |
while (oldHead <= oldTail) { | |
node.removeChild(oldVKids[oldHead++].node); | |
} | |
} else { | |
// keyを収集する | |
// old側にランダムアクセスできるようにする | |
// new側にはシーケンシャルアクセスする | |
for (var keyed = {}, newKeyed = {}, i = oldHead; i <= oldTail; i++) { | |
if ((oldKey = oldVKids[i].key) != null) { | |
keyed[oldKey] = oldVKids[i]; | |
} | |
} | |
while (newHead <= newTail) { | |
oldKey = getKey((oldVKid = oldVKids[oldHead])); | |
newKey = getKey((newVKids[newHead] = vdomify(newVKids[newHead]))); | |
/// ???ならoldを削除? | |
if ( | |
newKeyed[oldKey] || | |
(newKey != null && newKey === getKey(oldVKids[oldHead + 1])) | |
) { | |
if (oldKey == null) { | |
node.removeChild(oldVKid.node); | |
} | |
oldHead++; | |
continue; | |
} | |
if (newKey == null || oldVNode.type === SSR_NODE) { | |
if (oldKey == null) { | |
patchNode( | |
node, | |
oldVKid && oldVKid.node, | |
oldVKid, | |
newVKids[newHead], | |
isSvg | |
); | |
newHead++; | |
} | |
oldHead++; | |
} else { | |
if (oldKey === newKey) { | |
patchNode(node, oldVKid.node, oldVKid, newVKids[newHead], isSvg); | |
newKeyed[newKey] = true; | |
oldHead++; | |
} else { | |
if ((tmpVKid = keyed[newKey]) != null) { | |
patchNode( | |
node, | |
node.insertBefore(tmpVKid.node, oldVKid && oldVKid.node), | |
tmpVKid, | |
newVKids[newHead], | |
isSvg | |
); | |
newKeyed[newKey] = true; | |
} else { | |
patchNode( | |
node, | |
oldVKid && oldVKid.node, | |
null, | |
newVKids[newHead], | |
isSvg | |
); | |
} | |
} | |
newHead++; | |
} | |
} | |
while (oldHead <= oldTail) { | |
if (getKey((oldVKid = oldVKids[oldHead++])) == null) { | |
node.removeChild(oldVKid.node); | |
} | |
} | |
for (var i in keyed) { | |
if (newKeyed[i] == null) { | |
node.removeChild(keyed[i].node); | |
} | |
} | |
} | |
} | |
return (newVNode.node = node); | |
}; | |
var vdomify = (newVNode) => | |
newVNode !== true && newVNode !== false && newVNode ? newVNode : text(""); | |
var recycleNode = (node) => | |
node.nodeType === TEXT_NODE | |
? text(node.nodeValue, node) | |
: createVNode( | |
node.nodeName.toLowerCase(), | |
EMPTY_OBJ, | |
EMPTY_ARR.map.call(node.childNodes, recycleNode), | |
SSR_NODE, | |
node | |
); | |
var createVNode = (tag, props, children, type, node) => ({ | |
tag, | |
props, | |
key: props.key, | |
children, | |
type, | |
node, | |
}); | |
export var text = (value, node) => | |
createVNode(value, EMPTY_OBJ, EMPTY_ARR, TEXT_NODE, node); | |
export var h = (tag, props, children = EMPTY_ARR) => | |
createVNode(tag, props, Array.isArray(children) ? children : [children]); | |
export var patch = (node, vdom) => { | |
patchNode(node.parentNode, node, node.vdom || recycleNode(node), vdom); | |
requestAnimationFrame(() => { | |
for (let fn of fns) { | |
fn(); | |
} | |
fns = []; | |
}); | |
}; |
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
const ELEMENT_NODE = 1; | |
const TEXT_NODE = 3; | |
const EMPTY_OBJ = {}; | |
const EMPTY_ARR = []; | |
const SVG_NS = "http://www.w3.org/2000/svg"; | |
interface NodeWithV extends Node { | |
vnode: VNode; | |
} | |
interface VElement { | |
readonly props: any; // TODO | |
readonly children: VNode[]; | |
node: null | undefined | Node; // Native Node | |
readonly key: string | null | undefined; | |
readonly tag: string; | |
} | |
function newVElement( | |
tag: string, | |
props: any, | |
children: VNode[], | |
node: Node | |
): VElement { | |
return { | |
props, | |
children, | |
node, | |
key: props.key, | |
tag, | |
}; | |
} | |
type VNode = number | string | null | undefined | VElement | VNode[]; | |
function isTerm(vnode: VNode): boolean { | |
const t = typeof vnode; | |
return t === "number" || t === "string"; | |
} | |
function isVElement(vnode: VNode): vnode is VElement { | |
const t = typeof vnode; | |
return vnode != null && t !== "number" && t !== "string"; | |
} | |
function _patch( | |
parent: Node, | |
node: Node, | |
oldVNode: VNode, | |
newVNode: VNode, | |
isSvg: boolean | |
) { | |
if (oldVNode === newVNode) { | |
// do nothing | |
} else if ( | |
oldVNode != null && | |
!isVElement(oldVNode) && | |
!isVElement(newVNode) | |
) { | |
// テキストノードを更新 | |
} else if (oldVNode == null || oldVNode.tag !== newVNode.tag) { | |
// 要素作り直し | |
} else { | |
// 要素を更新 | |
} | |
} | |
function vnodify(node: Node): VNode { | |
return node.nodeType === TEXT_NODE | |
? node.nodeValue | |
: newVElement( | |
node.nodeName, | |
EMPTY_OBJ, | |
EMPTY_ARR.map.call(node.childNodes, vnodify), | |
node | |
); | |
} | |
function patch(node: NodeWithV, vnode: VNode) { | |
_patch(node.parentNode, node, node.vnode || vnodify(node), vnode, false); | |
} | |
function v(tag: string, props?: any) { | |
return (...children: VNode[]) => { | |
return newVElement(tag, props, children, null); | |
}; | |
} | |
v("div.class", { href: "#" })( | |
v("p")("Hello"), // | |
v("mark.strong")("World"), | |
v("ul.class")( | |
[1, 2, 3].map((e) => v("li")(e)) // | |
), | |
v("input", { type: "number" })() | |
); | |
const vContainer = v("div.container", { id: "main" }); | |
v("body")( | |
vContainer( | |
v("a", { href: "#" })("Click here!") // | |
) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment