Skip to content

Instantly share code, notes, and snippets.

@eihigh
Created September 18, 2021 02:02
Show Gist options
  • Save eihigh/72b3d52d2d5594363f8e232e618f4c9b to your computer and use it in GitHub Desktop.
Save eihigh/72b3d52d2d5594363f8e232e618f4c9b to your computer and use it in GitHub Desktop.
vdom
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 = [];
});
};
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