diff --git a/src/constants/constant.js b/src/constants/constant.js new file mode 100644 index 0000000..bb16870 --- /dev/null +++ b/src/constants/constant.js @@ -0,0 +1 @@ +export const eventTypes = ["click", "focus", "mouseover", "keydown"]; diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 5d39ae7..f7d8db7 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,5 +1,49 @@ -import { addEvent } from "./eventManager"; +// import { addEvent } from "./eventManager"; -export function createElement(vNode) {} +export function createElement(vNode) { + if (vNode == null || typeof vNode === "boolean" || vNode === undefined) { + return document.createTextNode(""); + } -function updateAttributes($el, props) {} + if (typeof vNode === "string" || typeof vNode === "number") { + return document.createTextNode(vNode); + } + + if (Array.isArray(vNode)) { + const fragement = document.createDocumentFragment(); + + vNode.forEach((child) => { + const childNode = createElement(child); + fragement.appendChild(childNode); + }); + return fragement; + } + + const $el = document.createElement(vNode.type); + updateAttributes($el, vNode.props); + if (vNode.children) { + vNode.children.forEach((child) => { + const childNode = createElement(child); + $el.appendChild(childNode); + }); + } else { + $el.appendChild(createElement(vNode.children)); + } + + return $el; +} + +function updateAttributes($el, props) { + if (!props) return; + + Object.entries(props).forEach(([key, value]) => { + if (key === "className") { + $el.setAttribute("class", value); + } else if (key.startsWith("on")) { + const eventType = key.slice(2).toLowerCase(); // 예: onClick -> click + $el.addEventListener(eventType, value); // 이벤트 핸들러 등록 + } else { + $el.setAttribute(key, value); // 일반 속성 처리 + } + }); +} diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 9991337..6c41800 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,3 +1,7 @@ -export function createVNode(type, props, ...children) { - return {}; +export function createVNode(type, props = {}, ...children) { + return { + type, + props, + children: children.flat(Infinity).filter((child) => child || child === 0), + }; } diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 24e4240..9450d83 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -1,5 +1,57 @@ -export function setupEventListeners(root) {} +import { eventTypes } from "../constants/constant"; -export function addEvent(element, eventType, handler) {} +const eventMap = new Map(); -export function removeEvent(element, eventType, handler) {} +export function setupEventListeners(root) { + eventTypes.forEach((eventType) => { + root.addEventListener(eventType, (event) => { + const target = event.target; + + for (const [element, handlers] of eventMap.entries()) { + if (element === target || element.contains(target)) { + const eventTypeHandlers = handlers.get("click"); + if (eventTypeHandlers) { + eventTypeHandlers.forEach((handler) => handler(event)); + } + } + } + }); + }); +} + +export function addEvent(element, eventType, handler) { + if (!eventMap.has(element)) { + eventMap.set(element, new Map()); + } + + const handlers = eventMap.get(element); + if (!handlers.has(eventType)) { + handlers.set(eventType, []); + } + + const handlerList = handlers.get(eventType); + if (!handlerList.includes(handler)) { + handlerList.push(handler); + } +} + +export function removeEvent(element, eventType, handler) { + if (!eventMap.has(element)) return; + + const handlers = eventMap.get(element); + if (handlers.has(eventType)) { + const handlerList = handlers.get(eventType); + const index = handlerList.indexOf(handler); + + if (index !== -1) { + handlerList[index] = null; + handlerList.splice(index, 1); + } + if (handlerList.length === 0) { + handlers.delete(eventType); + if (handlers.size === 0) { + eventMap.delete(element); + } + } + } +} diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 7dc6f17..5e29d30 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,3 +1,21 @@ export function normalizeVNode(vNode) { - return vNode; + if (typeof vNode === "number" || typeof vNode === "string") { + return String(vNode); + } + if (vNode === null || vNode === undefined || typeof vNode === "boolean") { + return ""; + } + + if (typeof vNode.type === "function") { + return normalizeVNode( + vNode.type({ ...vNode.props, children: vNode.children }), + ); + } + return { + ...vNode, + children: vNode.children + .filter((child) => child || child === 0) + .map((child) => normalizeVNode(child)) + .filter((child) => child !== ""), + }; } diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 0429572..9af12aa 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -3,8 +3,19 @@ import { createElement } from "./createElement"; import { normalizeVNode } from "./normalizeVNode"; import { updateElement } from "./updateElement"; +const vDom = new WeakMap(); + export function renderElement(vNode, container) { - // 최초 렌더링시에는 createElement로 DOM을 생성하고 - // 이후에는 updateElement로 기존 DOM을 업데이트한다. - // 렌더링이 완료되면 container에 이벤트를 등록한다. + const newVNode = normalizeVNode(vNode); + + if (!vDom.has(container)) { + const element = createElement(newVNode); + container.appendChild(element); + } else { + const oldVNode = vDom.get(container); + updateElement(container, newVNode, oldVNode); + } + + setupEventListeners(container); + vDom.set(container, newVNode); } diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index ac32186..e082ff1 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,6 +1,89 @@ import { addEvent, removeEvent } from "./eventManager"; import { createElement } from "./createElement.js"; -function updateAttributes(target, originNewProps, originOldProps) {} +function updateAttributes(target, originNewProps = {}, originOldProps = {}) { + if (originOldProps) { + Object.keys(originOldProps).forEach((key) => { + if (key.startsWith("on")) { + const eventType = key.slice(2).toLowerCase(); + const oldHandler = originOldProps[key]; -export function updateElement(parentElement, newNode, oldNode, index = 0) {} + if (typeof oldHandler === "function") { + removeEvent(target, eventType, oldHandler); + originOldProps[key] = null; + } + } else if (!(key in originNewProps)) { + target.removeAttribute(key); + } + }); + } + + if (originNewProps) { + Object.entries(originNewProps).forEach(([key, value]) => { + const oldValue = originOldProps[key]; + if (key === "className") { + if (value !== oldValue) { + target.className = value; + } + } else if (key.startsWith("on")) { + const eventType = key.slice(2).toLowerCase(); + if (value !== oldValue) { + if (typeof oldValue === "function") { + removeEvent(target, eventType, oldValue); + } + if (value) { + addEvent(target, eventType, value); + } else { + target.removeEventListener(eventType, oldValue); + } + } + } else if (key === "style" && typeof value === "object") { + Object.assign(target.style, value); + } else if (value !== oldValue) { + target.setAttribute(key, value); + } + }); + } +} + +export function updateElement(parentElement, newNode, oldNode, index = 0) { + if (!newNode && oldNode) { + parentElement.removeChild(parentElement.childNodes[index]); + return; + } + + if (newNode && !oldNode) { + const newElement = createElement(newNode); + parentElement.append(newElement); + return; + } + + if (typeof newNode === "string" && typeof oldNode === "string") { + if (newNode != oldNode) { + parentElement.childNodes[index].textContent = newNode; + } + return; + } + + if (newNode.type !== oldNode.type) { + const newElement = createElement(newNode); + const oldElement = parentElement.childNodes[index]; + if (oldElement) { + parentElement.replaceChild(newElement, parentElement.childNodes[index]); + return; + } + parentElement.appendChild(newElement); + return; + } + + const element = parentElement.childNodes[index]; + const newChildren = newNode.children; + const oldChildren = oldNode.children; + const maxLength = Math.max(newChildren.length, oldChildren.length); + + updateAttributes(element, newNode.props, oldNode.props); + + for (let i = 0; i < maxLength; i++) { + updateElement(element, newChildren[i], oldChildren[i], i); + } +}