https://pomb.us/build-your-own-react/ 을 기반으로 학습 내용을 정리한 글입니다.
STEP 8 - Hooks
이제 function components가 있으니 state를 추가해보는 작업을 해보겠다.
const Didact = {
createElement,
render,
useState,
};
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1);
return <h1 onClick={() => setState((c) => c + 1)}>Count: {state}</h1>;
}
const element = <Counter />;
const container = document.getElementById("root");
<h1> 영역이 클릭 될 때마다 상태가 1씩 증가하도록 구현해보자.
카운터 값을 가져오고 업데이트 하기 위해 Didact.useState를 사용하고 있다.
useState 함수 내에서 사용할 수 있도록 function component를 호출하기 전 일부 전역 변수를 초기화해야 한다.
먼저 진행중인 fiber를 설정한다.
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
동일한 component에서 useState를 여러번 호출하는 것을 지원하기 위해 fiber에 hooks 배열을 추가한다.
그리고 현재 hooks 인덱스를 추적한다. hook을 저장 및 관리하기 위해 hookIndex와 hooks 배열을 초기화한다.
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
function component가 useState를 호출하면 이전 hooks이 있는지 확인한다.
wipFiber.alternate는 현재 작업중인 fiber의 이전 버전을 가리킨다.
wipFiber.alternate.hooks[hookIndex]에서 이전 hook을 확인하고, 있다면 이전 state 값을 사용해 새 훅의 상태로 설정한다.
이전 hook이 없다면 initial 값을 초기 상태로 설정한다.
그런 다음 fiber에 새 hook을 추가하고 hook 인덱스를 1 증가시킨 후 다음 state를 반환한다.
function useState(initial) {
// 이전 렌더링에서 저장된 hook 상태가 있는지 확인
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
// hook 객체 생성 (이전 상태가 없으면 initial로 설정)
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
// 상태를 업데이트하는 함수
const setState = (action) => {
hook.queue.push(action); // 상태 업데이트 액션을 queue에 추가
// 현재 루트를 복제하여 새로운 작업 루트(wipRoot) 생성
wipRoot = {
dom: currentRoot.dom, // 현재 DOM을 복사
props: currentRoot.props, // 현재 props를 복사
alternate: currentRoot, // 현재 루트를 이전 루트로 설정 (트리 연결)
};
// 다음 작업을 새로 생성된 wipRoot로 설정
nextUntilOfWork = wipRoot;
deletions = []; // 삭제 목록 초기화
};
// 현재 hook을 fiber의 hook 배열에 추가하고, hook 인덱스 증가
wipFiber.hooks.push(hook);
hookIndex++;
// 상태와 상태 업데이트 함수를 반환
return [hook.state, setState];
}
useState는 state를 업데이트 하는 함수도 반환해야 하므로 작업을 수행하는 setState 함수를 정의한다.
우선, queue를 추가했다. queue는 setState에 파라미터로 전달된 action들을 저장하는 역할이다.
React가 한번의 렌더링 주기에서 여러번의 setState 호출을 받는 경우, 모든 업데이트를 즉시 반영하지 않고 일괄처리하는 것이 효율적이기 때문이다.
queue는 상태 변경 요청을 모아서 한 번에 처리하기 때문에 여러 setState 호출로 인한 불필요한 렌더링을 방지하고 성능을 최적화한다.
우리는 hook에 추가한 queue에 해당 작업을 push 한다.
그 다음 렌더링 함수에서 수행한 것과 유사한 작업을 수행하고 work loop가 새 렌더링 단계를 시작할 수 있도록 진행중인 새 작업 루트를 다음 작업 단위로 설정한다.
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
다음 컴포넌트를 렌더링 할 때 이 작업을 수행한다.
이전 hook 대기열에서 모든 작업을 가져온 다음 새 훅 state에 하나씩 적용하므로 상태를 반환하면 업데이트 된다.
즉, STEP 8을 완료한 상태의 전체 코드는 아래와 같다.
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
function createDom(fiber) {
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
updateDom(dom, {}, fiber.props);
return dom;
}
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (prev, next) => (key) => !(key in next);
function updateDom(dom, prevProps, nextProps) {
// Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach((name) => {
dom[name] = "";
});
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name];
});
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function commitRoot() {
deletions.forEach(commitWork);
// add nodes to dom
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUntilOfWork = wipRoot;
}
let nextUntilOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUntilOfWork && !shouldYield) {
nextUntilOfWork = performUnitOfWork(nextUntilOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUntilOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
const setState = (action) => {
hook.queue.push(action);
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUntilOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
// compare oldFiber to element
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
// update the node
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
if (element && !sameType) {
// add this node
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
// delete the oldFiber's node
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
const Didact = {
createElement,
render,
useState,
};
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1);
return <h1 onClick={() => setState((c) => c + 1)}>Count: {state}</h1>;
}
const container = document.getElementById("root");
const element = <Counter />;
Didact.render(element, container);
여기까지 STEP 8 완료! 마무리!
'FRONT-END > React' 카테고리의 다른 글
memo, useMemo 이해하기 (1) | 2024.09.11 |
---|---|
나만의 React 만들기(Build your own react) - STEP +α (0) | 2024.09.10 |
나만의 React 만들기(Build your own react) - STEP 7 (0) | 2024.09.07 |
나만의 React 만들기(Build your own react) - STEP 6 (1) | 2024.09.06 |
나만의 React 만들기(Build your own react) - STEP 5 (0) | 2024.09.06 |
댓글