Skip to content

Commit 055c113

Browse files
committed
feat: mini-react 구현(fiber, useState)
1 parent ce14d36 commit 055c113

15 files changed

+10958
-0
lines changed

.babelrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
3+
"plugins": ["@babel/plugin-transform-runtime"]
4+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/node_modules

commitUpdate.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { context } from "./context";
2+
import { Fiber } from "./fiber";
3+
4+
export function commitRootFiber(rootFiber: Fiber) {
5+
if (!rootFiber.child) return;
6+
7+
context.deletedQueue.forEach((deletion) => commitFiberUpdate(deletion));
8+
context.deletedQueue = [];
9+
10+
commitFiberUpdate(rootFiber.child);
11+
12+
context.recentlyCommittedRootFiber = context.unCommitedRootFiber;
13+
context.unCommitedRootFiber = null;
14+
}
15+
16+
export function commitFiberUpdate(fiber: Fiber) {
17+
let parentFiber: Nullable<Fiber> = fiber.parent!;
18+
while (parentFiber && parentFiber.dom === null) {
19+
parentFiber = parentFiber.parent;
20+
}
21+
22+
const parentDom = parentFiber!.dom!;
23+
24+
if (fiber.effectTag === "PLACEMENT" && fiber.dom) {
25+
commitAddDom(parentDom, fiber);
26+
}
27+
28+
if (fiber.effectTag === "UPDATE" && fiber.dom) {
29+
commitUpdateDomProerty(fiber.dom, fiber.alternate?.props, fiber.props);
30+
}
31+
32+
if (fiber.effectTag === "DELETE") {
33+
commitDeleteDom(parentDom, fiber);
34+
}
35+
36+
if (fiber.child) commitFiberUpdate(fiber.child);
37+
if (fiber.sibling) commitFiberUpdate(fiber.sibling);
38+
}
39+
40+
export function commitUpdateDomProerty(
41+
dom: Node,
42+
prevProps: Record<string, unknown> = {},
43+
nextProps: Record<string, unknown> = {}
44+
) {
45+
commitDeletedEventListener(dom, prevProps, nextProps);
46+
commitAddedEventListener(dom, prevProps, nextProps);
47+
commitDeletedProperty(dom, prevProps, nextProps);
48+
commitAddedProperty(dom, prevProps, nextProps);
49+
}
50+
51+
export function commitAddDom(parent: Node, child: Fiber) {
52+
parent.appendChild(child.dom!);
53+
}
54+
export function commitDeleteDom(parent: Node, child: Fiber) {
55+
while (!child.dom) child = child.child!;
56+
parent.removeChild(child.dom);
57+
}
58+
59+
export function isEvent(key: string) {
60+
return key.startsWith("on");
61+
}
62+
63+
export function isProperty(key: string) {
64+
return key !== "children" && !isEvent(key);
65+
}
66+
67+
export function isNewProperty(prevProps: Record<string, unknown>, nextProps: Record<string, unknown>) {
68+
return function (key: string) {
69+
return prevProps[key] !== nextProps[key];
70+
};
71+
}
72+
73+
export function isDeletedProperty(prevProps: Record<string, unknown>, nextProps: Record<string, unknown>) {
74+
return function (key: string) {
75+
return key in prevProps && !(key in nextProps);
76+
};
77+
}
78+
79+
export function getEventNameFromOnEvent(onEventName: string) {
80+
return onEventName.substring(2).toLowerCase();
81+
}
82+
83+
export function commitDeletedEventListener(
84+
dom: Node,
85+
prevProps: Record<string, unknown> = {},
86+
nextProps: Record<string, unknown> = {}
87+
) {
88+
Object.keys(prevProps)
89+
.filter(isEvent)
90+
.filter((key) => isDeletedProperty(prevProps, nextProps)(key) || isNewProperty(prevProps, nextProps)(key))
91+
.forEach((key) =>
92+
dom.removeEventListener(
93+
getEventNameFromOnEvent(key),
94+
prevProps[key] as unknown as EventListenerOrEventListenerObject
95+
)
96+
);
97+
}
98+
99+
export function commitAddedEventListener(
100+
dom: Node,
101+
prevProps: Record<string, unknown> = {},
102+
nextProps: Record<string, unknown> = {}
103+
) {
104+
Object.keys(nextProps)
105+
.filter(isEvent)
106+
.filter((key) => isNewProperty(prevProps, nextProps)(key))
107+
.forEach((key) =>
108+
dom.addEventListener(
109+
getEventNameFromOnEvent(key),
110+
nextProps[key] as unknown as EventListenerOrEventListenerObject
111+
)
112+
);
113+
}
114+
115+
export function commitDeletedProperty(
116+
dom: Node,
117+
prevProps: Record<string, unknown> = {},
118+
nextProps: Record<string, unknown> = {}
119+
) {
120+
Object.keys(prevProps)
121+
.filter(isProperty)
122+
.filter((key) => isDeletedProperty(prevProps, nextProps)(key))
123+
.forEach((key) => delete dom[key as keyof Node]);
124+
}
125+
126+
export function commitAddedProperty(
127+
dom: Node,
128+
prevProps: Record<string, unknown> = {},
129+
nextProps: Record<string, unknown> = {}
130+
) {
131+
Object.keys(nextProps)
132+
.filter(isProperty)
133+
.filter((key) => isNewProperty(prevProps, nextProps)(key))
134+
.forEach((key) => (dom[key] = nextProps[key]));
135+
}

context.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Fiber } from "./fiber";
2+
3+
type Context = {
4+
recentlyCommittedRootFiber: Nullable<Fiber>;
5+
unCommitedRootFiber: Nullable<Fiber>;
6+
workInProgressFiber: Nullable<Fiber>;
7+
hookIndex: number;
8+
deletedQueue: Fiber[];
9+
};
10+
11+
export const context: Context = {
12+
recentlyCommittedRootFiber: null,
13+
unCommitedRootFiber: null,
14+
workInProgressFiber: null,
15+
hookIndex: 0,
16+
deletedQueue: [],
17+
};
18+
19+
export function canCommit(context: Context) {
20+
return context.workInProgressFiber === null && context.unCommitedRootFiber !== null;
21+
}
22+
23+
export function isWorkInProgress(workInProgressFiber: Fiber | null): workInProgressFiber is Fiber {
24+
return workInProgressFiber !== null;
25+
}

fiber.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
export type Fiber = {
2+
type: any;
3+
alternate: Nullable<Fiber>;
4+
child: Nullable<Fiber>;
5+
sibling: Nullable<Fiber>;
6+
parent: Nullable<Fiber>;
7+
effectTag: Nullable<"UPDATE" | "DELETE" | "PLACEMENT">;
8+
hooks: Nullable<any[]>;
9+
dom: Nullable<Node>;
10+
props: Record<string, unknown> & { children: Fiber[] };
11+
};
12+
13+
export function createFiber({
14+
type,
15+
alternate = null,
16+
child = null,
17+
sibling = null,
18+
parent = null,
19+
effectTag = null,
20+
hooks = null,
21+
dom = null,
22+
props = { children: [] },
23+
}: Partial<Fiber>): Fiber {
24+
return {
25+
type,
26+
alternate,
27+
child,
28+
sibling,
29+
parent,
30+
effectTag,
31+
hooks,
32+
dom,
33+
props,
34+
};
35+
}
36+
37+
export function getNextFiber(fiber: Fiber) {
38+
if (fiber.child) return fiber.child;
39+
40+
if (fiber.sibling) return fiber.sibling;
41+
42+
let nextFiber: Fiber | null = fiber;
43+
for (; nextFiber !== null; nextFiber = nextFiber.parent) {
44+
if (nextFiber.sibling) {
45+
return nextFiber.sibling;
46+
}
47+
}
48+
49+
return null;
50+
}
51+
52+
export function isFunctionComponent(fiber: Fiber) {
53+
return fiber.type instanceof Function;
54+
}
55+
56+
export function hasFiberHasEmptyChildren(fiber: Fiber) {
57+
return fiber.props.children.length === 0;
58+
}

fiberReconcil.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { context } from "./context";
2+
import { createFiber, Fiber, hasFiberHasEmptyChildren } from "./fiber";
3+
4+
const FirstChild = 0;
5+
6+
export function reconcilFiberChildren(fiber: Fiber) {
7+
if (!fiber.alternate) {
8+
reconcilReplacedChildren(fiber, fiber.props.children);
9+
return;
10+
}
11+
12+
if (hasFiberHasEmptyChildren(fiber)) {
13+
reconcilDeletedChildren(fiber.alternate.child);
14+
return;
15+
}
16+
17+
reconcilChildrenCompareToAlternate(fiber, fiber.props.children, fiber.alternate.child);
18+
}
19+
20+
export function reconcilReplacedChildren(
21+
parent: Fiber,
22+
children: Fiber[],
23+
from: number = 0,
24+
prevSibling: Nullable<Fiber> = null
25+
) {
26+
for (let idx = from; idx < children.length; idx++) {
27+
const child = children[idx];
28+
29+
const newFiber = createFiber({
30+
type: child.type,
31+
props: child.props,
32+
parent,
33+
effectTag: "PLACEMENT",
34+
});
35+
36+
if (idx === FirstChild) {
37+
parent.child = newFiber;
38+
prevSibling = newFiber;
39+
continue;
40+
}
41+
42+
prevSibling!.sibling = newFiber;
43+
prevSibling = newFiber;
44+
}
45+
}
46+
47+
export function reconcilDeletedChildren(alternate: Nullable<Fiber>, prevSibling: Nullable<Fiber> = null) {
48+
while (alternate) {
49+
alternate.effectTag = "DELETE";
50+
if (prevSibling) {
51+
prevSibling.sibling = alternate;
52+
}
53+
prevSibling = alternate;
54+
alternate = alternate.sibling;
55+
}
56+
}
57+
58+
export function reconcilChildrenCompareToAlternate(
59+
parent: Fiber,
60+
children: Fiber[],
61+
alternate: Nullable<Fiber>
62+
) {
63+
let prevSibling: Nullable<Fiber> = null;
64+
let idx = 0;
65+
66+
const childrenLength = children.length;
67+
68+
while (idx < childrenLength && alternate) {
69+
const child = children[idx];
70+
const newFiber = getNewFiberByCompare(parent, alternate, child);
71+
72+
if (newFiber.effectTag === "PLACEMENT") {
73+
alternate.effectTag = "DELETE";
74+
context.deletedQueue.push(alternate);
75+
}
76+
77+
if (idx === FirstChild) {
78+
parent.child = newFiber;
79+
prevSibling = newFiber;
80+
idx++;
81+
alternate = alternate.sibling;
82+
continue;
83+
}
84+
85+
idx++;
86+
alternate = alternate.sibling;
87+
prevSibling!.sibling = newFiber;
88+
prevSibling = newFiber;
89+
}
90+
91+
reconcilDeletedChildren(alternate, prevSibling);
92+
reconcilReplacedChildren(parent, children, idx, prevSibling);
93+
}
94+
95+
export function getNewFiberByCompare(parent: Fiber, alternate: Fiber, child: Fiber) {
96+
return isUpdated(alternate, child)
97+
? createFiber({
98+
type: alternate.type,
99+
props: child.props,
100+
dom: alternate.dom,
101+
parent,
102+
alternate: alternate,
103+
effectTag: "UPDATE",
104+
})
105+
: createFiber({
106+
type: child.type,
107+
props: child.props,
108+
parent,
109+
effectTag: "PLACEMENT",
110+
});
111+
}
112+
113+
export function isUpdated(oldFiber: Fiber, newFiber: Fiber) {
114+
return oldFiber.type === newFiber.type;
115+
}

fiberUpdates.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Fiber, getNextFiber, isFunctionComponent } from "./fiber";
2+
import { canCommit, context, isWorkInProgress } from "./context";
3+
import { reconcilFiberChildren } from "./fiberReconcil";
4+
import { commitAddedEventListener, commitAddedProperty, commitRootFiber } from "./commitUpdate";
5+
6+
export function updateRootFiberIfIdle(deadLine: IdleDeadline) {
7+
let shouldYield = false;
8+
while (context.workInProgressFiber && !shouldYield) {
9+
context.workInProgressFiber = updateFiberByType(context.workInProgressFiber);
10+
}
11+
shouldYield = deadLine.timeRemaining() < 1;
12+
13+
if (canCommit(context)) {
14+
commitRootFiber(context.unCommitedRootFiber!);
15+
return;
16+
}
17+
18+
requestIdleCallback(updateRootFiberIfIdle);
19+
}
20+
21+
export function updateFiberByType(fiber: Fiber) {
22+
const updateComponentFunction = isFunctionComponent(fiber) ? updateFunctionComponent : updateHostComponent;
23+
24+
updateComponentFunction(fiber);
25+
26+
return getNextFiber(fiber);
27+
}
28+
29+
export function updateFunctionComponent(fiber: Fiber) {
30+
if (!isWorkInProgress(context.workInProgressFiber)) return;
31+
context.workInProgressFiber.hooks = [];
32+
context.hookIndex = 0;
33+
34+
fiber.props.children = [fiber.type(fiber.props)];
35+
reconcilFiberChildren(fiber);
36+
}
37+
export function updateHostComponent(fiber: Fiber) {
38+
fiber.dom = fiber.dom ?? createDom(fiber);
39+
40+
reconcilFiberChildren(fiber);
41+
}
42+
43+
export function createDom(fiber: Fiber) {
44+
const dom: Node =
45+
fiber.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type);
46+
47+
commitAddedProperty(dom, {}, fiber.props);
48+
commitAddedEventListener(dom, {}, fiber.props);
49+
50+
return dom;
51+
}

global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
type Nullable<T> = T | null;

0 commit comments

Comments
 (0)