Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12팀 배성규] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #34

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions src/constants/constant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const eventTypes = ["click", "focus", "mouseover", "keydown"];
50 changes: 47 additions & 3 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성규님~! 혹시 이벤트 핸들러 등록하는부분을 eventManager 에서 만드신 addEvent함수를 이용하는것에대해서 어떻게생각하시나요??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수를 제가 만들어놓고도 쓰지를 않고 있었네요,, ㅋㅋ 피드백 감사합니다 !

Copy link

@feel5ny feel5ny Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 성규님-! 김나영 코치입니다.
리뷰해주신 분의 말씀대로 해당 부분은 매니저에서 만들어둔 addEvent를 사용하셔야 현재 fail나는 테스트코드가 해결될 것으로 보입니다. 아마 addEvent에서 Map자료구조에 이벤트가 등록이 되어있다는 가정하에 removeEvent가 동작했을텐데, 직접 요소에서 이벤트가 관리되고 있어 removeEvent로는 제거처리가 되지 않은듯합니다 :)

수정하시면 다른 TC가 아마 깨지실텐데요, 하나씩 풀어나가시면서 리팩토링을 해보시면 좋을 듯 합니다.

고생하셨습니다 :) @pangkyu

$el.setAttribute(key, value); // 일반 속성 처리
}
});
}
8 changes: 6 additions & 2 deletions src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -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),
};
}
58 changes: 55 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
20 changes: 19 additions & 1 deletion src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -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 !== ""),
};
}
17 changes: 14 additions & 3 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
87 changes: 85 additions & 2 deletions src/lib/updateElement.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading