From 29ec3bad63edf0aef41083683b0f3131e7327ca0 Mon Sep 17 00:00:00 2001 From: nopinokio Date: Fri, 27 Dec 2024 02:44:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20updateElement=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 47 ++++++++++---------- src/lib/eventManager.js | 6 ++- src/lib/renderElement.js | 13 +++++- src/lib/updateElement.js | 92 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 28 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 44442f2..eb56dfb 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -2,44 +2,45 @@ import { isValidVNode, isString, isNumber } from "./validCheck"; import { addEvent } from "./eventManager"; export function createElement(vNode) { - // null, undefined, boolean > 빈 텍스트 노드 if (!isValidVNode(vNode)) { return document.createTextNode(""); } - // 텍스트와 숫자 > 텍스트 노드 + if (isString(vNode) || isNumber(vNode)) { return document.createTextNode(vNode); } if (Array.isArray(vNode)) { - const fragment = document.createDocumentFragment(); - vNode.forEach((child) => fragment.appendChild(createElement(child))); - return fragment; + const $fragment = document.createDocumentFragment(); + vNode.forEach((child) => $fragment.appendChild(createElement(child))); + return $fragment; + } + + if (typeof vNode.type === "function") { + throw new Error("Function Components are not supported."); } - const $el = document.createElement(vNode.type); + const $element = document.createElement(vNode.type); + + updateAttributes($element, vNode.props ?? {}); - $el.append(...vNode.children.map(createElement)); - updateAttributes($el, vNode.props); + $element.append(...vNode.children.map(createElement)); - return $el; + return $element; } /** * DOM 요소에 속성 추가 */ -function updateAttributes(element, props) { - Object.entries(props || {}) - //.filter(([attr, value]) => value) - .forEach(([attr, value]) => { - // className > class속성으로 변경 - if (attr === "className") { - element.setAttribute("class", value); - } else if (attr.startsWith("on")) { - const eventType = attr == "onClick" ? "click" : attr; - addEvent(element, eventType, value); - } else { - element.setAttribute(attr, value); - } - }); +function updateAttributes($element, props) { + Object.entries(props).forEach(([attr, value]) => { + if (attr === "className") { + $element.setAttribute("class", value); + } else if (attr.startsWith("on") && typeof value === "function") { + const eventType = attr.toLowerCase().slice(2); + addEvent($element, eventType, value); + } else { + $element.setAttribute(attr, value); + } + }); } diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 8a8f78d..b9c7ac2 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -42,7 +42,11 @@ export function removeEvent(element, eventType, handler) { } }); - if (!matchingEvent) return; //값이 없으면 그대로 종료 + if (!matchingEvent) return; + + // eventRegistry에서 matchingEvent를 제거 제거 + const index = eventRegistry.indexOf(matchingEvent); + eventRegistry.splice(index, 1); if (matchingEvent !== null) { rootElement.removeEventListener(eventType, matchingEvent.handler); diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 8064eed..4699deb 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -9,6 +9,15 @@ export function renderElement(vNode, container) { // 최초 렌더링시에는 createElement로 DOM을 생성하고 // 이후에는 updateElement로 기존 DOM을 업데이트한다. // 렌더링이 완료되면 container에 이벤트를 등록한다. - oldVNode = normalizeVNode(vNode); - container.appendChild(createElement(oldVNode)); + + if (!container.firstChild) { + oldVNode = normalizeVNode(vNode); + container.appendChild(createElement(oldVNode)); + } else { + const newVNode = normalizeVNode(vNode); + updateElement(container, newVNode, oldVNode, 0); + oldVNode = newVNode; + } + + setupEventListeners(container); } diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index ac32186..d32bfda 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,6 +1,94 @@ import { addEvent, removeEvent } from "./eventManager"; import { createElement } from "./createElement.js"; -function updateAttributes(target, originNewProps, originOldProps) {} +/* + * popstate실행, 상태 변경 될 때마다 notify()를 호출, 실행하도록 createObserver의 subscribe()를 통해 등록이 돼있음 + * 따라서 render함수가 호출될 때마다 newNode와 oldNode를 비교 후 업데이트 해주어야 함 + * */ +export function updateElement(parentElement, newNode, oldNode, index = 0) { + // 노드 삭제 + if (!newNode && oldNode) { + parentElement.removeChild(parentElement.childNodes[index]); + return; + } -export function updateElement(parentElement, newNode, oldNode, index = 0) {} + // 노드 새로 추가 + if (newNode && !oldNode) { + parentElement.appendChild(createElement(newNode)); + return; + } + + // 노드 타입 변경 + if (newNode.type !== oldNode.type) { + return parentElement.replaceChild( + createElement(newNode), + parentElement.childNodes[index], + ); + } + + // 텍스트 노드 변경 + if (typeof newNode === "string" && typeof oldNode === "string") { + if (newNode === oldNode) return; + return parentElement.replaceChild( + createElement(newNode), + parentElement.childNodes[index], + ); + } + + updateAttributes( + parentElement.childNodes[index], + newNode.props || {}, + oldNode.props || {}, + ); + + // 재귀적으로 자식 노드 비교 + const maxLength = Math.max(newNode.children.length, oldNode.children.length); + for (let i = 0; i < maxLength; i++) { + updateElement( + parentElement.childNodes[index], + newNode.children[i], + oldNode.children[i], + i, + ); + } +} + +function updateAttributes(target, originNewProps, originOldProps = {}) { + // 이전 속성들 제거 + for (const attr in originOldProps) { + if (attr === "children") continue; + + // 이벤트 리스너인 경우 + if (attr.startsWith("on")) { + const eventType = attr.toLowerCase().substring(2); + removeEvent(target, eventType, originOldProps[attr]); + continue; + } + + // 새로운 props에 없는 경우 속성 제거 + if (!(attr in originNewProps)) { + target.removeAttribute(attr); + } + } + + // 새로운 속성 추가/업데이트 + for (const attr in originNewProps) { + if (attr === "children") continue; + + // 이벤트 리스너인 경우 + if (attr.startsWith("on")) { + const eventType = attr.toLowerCase().substring(2); + addEvent(target, eventType, originNewProps[attr]); + continue; + } + + // 값이 변경된 경우만 업데이트 + if (originOldProps[attr] !== originNewProps[attr]) { + if (attr === "className") { + target.setAttribute("class", originNewProps[attr]); + } else { + target.setAttribute(attr, originNewProps[attr]); + } + } + } +}