└── README.md
/README.md:
--------------------------------------------------------------------------------
1 | # 나만의 React 만들기
2 | https://pomb.us/build-your-own-react/
3 |
4 | 위 블로그 보며 자신만의 리액트 만들어보기
5 |
6 | 학습 흐름에 맞추어 작성하였다.
7 |
8 | ## 0. preview
9 | 리액트의 기본 예시는 다음과 같다.
10 | ``` js
11 | const element =
Hello
;
12 | const container = document.getElementById("root");
13 | ReactDom.render(element, container);
14 | ```
15 |
16 | 이를 순수 자바 스크립트 함수로 바꾸면 다음과 같다.
17 | ```js
18 | const element = {
19 | type: "h1",
20 | props: {
21 | title: "foo",
22 | children: "Hello",
23 | },
24 | };
25 | const container = document.getElementById("root");
26 | ```
27 |
28 | jsx는 보통 babel을 통해서 js로 트랜스파일링 된다.
29 |
30 | js로 사용하려면 createElement로 대체 한다. 이 element는 type과 props로 이루어져있다.(사실 더 많은 것이 있지만)
31 |
32 | type은 dom의 타입을 나타내며, 함수가 될 수도 있다.
33 |
34 | props는 jsx의 어트리뷰트로 받은 키와 값들이다.
35 |
36 | children은 자식으로 보통은 또 다른 엘리먼트들의 배열이다.
37 |
38 | 리액트의 render는 react element를 실제 javascript dom으로 바꾸는 함수이다.
39 | ```js
40 | const node = document.createElement(element.type);
41 | node["title"] = element.props.title;
42 |
43 | const text = document.createTextNode("");
44 | text["nodeValue"] = element.props.children;
45 |
46 | node.appendChild(text);
47 | container.appendChild(node);
48 | ```
49 |
50 | ## 1. createElement
51 | 이제 우리만의 React를 만들어보자.
52 |
53 | 먼저 앞서 보았듯이 type, props, children을 갖는 element를 만드는 함수를 만들자.
54 | ```js
55 | function createElement(type, props, ...children) {
56 | return {
57 | type,
58 | props: {
59 | ...props,
60 | children: children.map((child) => {
61 | typeof child == "object" ? child : createTextElement(child);
62 | }),
63 | },
64 | };
65 | }
66 | ```
67 |
68 | children이 원시값일 때를 처리하는 wrap 함수(createTextElement)를 만든다.
69 |
70 | 참고로, 실제 React는 children에 원시값이나 빈 배열을 할당하지는 않는다.
71 | ```js
72 | function createTextElement(text) {
73 | return {
74 | type: "TEXT_ELEMENT",
75 | props: {
76 | nodeValue: text,
77 | children: [],
78 | },
79 | };
80 | }
81 | ```
82 |
83 | 우리 라이브러리의 이름은 myReact 라고 해두자
84 | ```js
85 | const myReact = {
86 | createElement,
87 | };
88 | ```
89 |
90 | element를 생성한다면 다음과 같을 것이다.
91 | ```js
92 | const element = myReact.createElement(
93 | "div",
94 | { id: "foo" },
95 | myReact.createElement("a", null, "bar"),
96 | myReact.createElement("b")
97 | );
98 | ```
99 |
100 | 만약, 우리가 JSX는 계속 쓰고 싶다면,
101 |
102 | babel에게 React의 createElement가 아닌 우리 creataeElement를 쓰라고 다음 커멘트를 달아야한다.
103 | ```js
104 | /** @jsx myReact.createElement */
105 | const element = (
106 |
110 | );
111 | ```
112 |
113 | ## 2. render
114 | 다음은 render 함수를 만들어보자.
115 |
116 | 우선, DOM에 추가하는 코드만 만들고, 차례대로 업데이트, 삭제를 구현할 것이다.
117 |
118 | DOM 노드를 만들고 type을 정하고, container에 추가하는 함수를 만들자.
119 |
120 | 자식들도 재귀적으로 element를 생성해줘야한다.
121 |
122 | Text element에 대한 제외 처리도 해준다.
123 |
124 | 마지막으로, element의 children 외의 props들을 dom 노드에 추가해준다.
125 | ```js
126 | function render(element, container) {
127 | const dom =
128 | element.type == "TEXT_ELEMENT"
129 | ? document.createTextNode("")
130 | : document.createElement(element.type);
131 |
132 | const isPropety = (key) => key !== "children";
133 |
134 | Object.keys(element.props)
135 | .filter(isPropety)
136 | .forEach((name) => {
137 | dom[name] = element.props[name];
138 | });
139 |
140 | element.props.children.forEach((child) => render(child, dom));
141 | container.appendChild(dom);
142 | }
143 | ```
144 |
145 | ## 3. concurrent Mode
146 | 다음 코드를 작성하기 하기전에 짚어야 할게 있다.
147 |
148 | 이전 render 함수의 재귀 코드는 많은 양의 트리를 처리할 때는 오랫동안 블록을 하고 있어서 다른 작업을 할 수가 없다.
149 | ```js
150 | Object.keys(element.props)
151 | .filter(isPropety)
152 | .forEach((name) => {
153 | dom[name] = element.props[name];
154 | });
155 | ```
156 |
157 | 그래서, 이 작업을 작은 단위로 쪼개서 브라우저가 다른 작업이 필요하면 멈출 수 있게 하도록 할 것이다.
158 |
159 | 우리는 requestIdleCallback로 루프를 만들건데, requestIdleCallback은 브라우저가 쓰레드가 작업이 없을 때 실행시키도록 하는 함수이다.
160 |
161 | 실제 React는 이를 더 이상 사용하지 않고 스케쥴러를 사용하지만 우리 대체해서 사용하도록한다. (자세한 내용은 concurrent 모드에 대해 알아보면 좋음)
162 |
163 | 또한, requestIdleCallback은 deadline 파라미터를 제공한다. 이를 통해 브라우저가 다신 제어권을 가져갈때까지 얼마나 남았는지 알 수 있다.
164 |
165 | ```js
166 | function workLoop(deadline) {
167 | let shouldYield = false;
168 | while (nextUnitOfWork && !shouldYield) {
169 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
170 | shouldYield = deadline.timeRemaining() < 1;
171 | }
172 | requestIdleCallback(workLoop);
173 | }
174 | ```
175 |
176 | 이 workLoop를 더 완성시켜보자.
177 |
178 | performUnitOfWork는 다음 작업 단위를 리턴하는 함수로, 일단 todo로 남겨놓자.
179 |
180 | ```js
181 | let nextUnitOfWork = null;
182 |
183 | function workLoop(deadline) {
184 | let shouldYield = false;
185 | while (nextUnitOfWork && !shouldYield) {
186 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
187 | shouldYield = deadline.timeRemaining() < 1;
188 | }
189 | requestIdleCallback(workLoop);
190 | }
191 |
192 | requestIdleCallback(workLoop);
193 |
194 | function performUnitOfWork(nextUnitOfWork) {
195 | // todo
196 | }
197 | ```
198 |
199 | ## 4. fibers
200 | 이 실행 단위를 관리하기 위한 자료구조가 바로 fiber이다.
201 |
202 | element 마다 fiber를 가지게 되고, 이 fiber는 하나의 실행 단위이다.
203 |
204 | 다음과 같은 html이 있다고하자
205 | ``` js
206 | Didact.render(
207 |
208 |
209 |
210 |
211 |
212 |
213 |
,
214 | container
215 | )
216 | ```
217 | render 함수는 root fiber를 만들고 첫 nextUnitOfWork로 설정할 것이다.
218 |
219 | 이후 작업들은 performUnitOfWork에서 실행될 것이고 다음과 같다.
220 | ```
221 | 1. Dom에 element 달기
222 | 2. children에 대한 fiber 생성
223 | 3. 다음 실행 단위 설정
224 | ```
225 |
226 | 이 자료구조의 핵심은 다음 실행 단위를 쉽게 찾는 것이다.
227 |
228 | 그래서, fiber들은 첫 번째 자녀, 바로 옆 형제, 부모와의 링크를 가지고 있고 다음과 같은 규칙을 가진다.
229 | ```
230 | 자식 있다면, 자식이 다음 실행 단위가 된다.
231 | 자식이 없다면, 형제가 실행 단위가 된다.
232 | 자식, 형제가 없다면, 삼촌(형제의 부모)가 실행 단위가 된다.
233 | ```
234 |
235 | 이를 코드로 옮겨보자. 우선 기존 render 함수에서 dom을 만드는 부분을 따로 함수화한다.
236 | ```js
237 | function createDom(fiber) {
238 | const dom =
239 | fiber.type == "TEXT_ELEMENT"
240 | ? document.createTextNode("")
241 | : document.createElement(fiber.type);
242 |
243 | const isPropety = (key) => key !== "children";
244 |
245 | Object.keys(fiber.props)
246 | .filter(isPropety)
247 | .forEach((name) => {
248 | dom[name] = fiber.props[name];
249 | });
250 |
251 | return dom;
252 | }
253 | ```
254 |
255 | render 함수는 이제 nextUnifOfWork를 root의 fiber tree로 설정한다.
256 | ```js
257 | function render(element, container) {
258 | nextUnitOfWork = {
259 | dom: container,
260 | props: {
261 | children: [element],
262 | },
263 | };
264 | }
265 | ```
266 |
267 | 이제, 브라우저가 준비가 되면 workLoop를 호출해 root부터 작업을 시작할 것이다.
268 |
269 | 먼저, 새 node를 만들어 dom에 붙이고,
270 |
271 | 자식들에 대해 fiber를 생성한다.
272 | 첫 자녀는 자녀로 연결하고, 나머지들은 서로 형제로 연결한다.
273 |
274 | 마지막으로는, 다음 실행 단위를 찾고 반환한다.
275 |
276 | ```js
277 | function performUnitOfWork(fiber) {
278 | if (!fiber.dom) {
279 | fiber.dom = createDom(fiber);
280 | }
281 |
282 | if (fiber.parent) {
283 | fiber.parent.dom.appendChild(fiber.dom);
284 | }
285 |
286 | const elements = fiber.props.children;
287 | let index = 0;
288 | let prevSibling = null;
289 |
290 | while (index < elements.lenth) {
291 | const element = elements[index];
292 | const newFiber = {
293 | type: element.type,
294 | props: element.props,
295 | parent: fiber,
296 | dom: null,
297 | };
298 |
299 | if (index == 0) {
300 | fiber.child = newFiber;
301 | } else {
302 | prevSibling.sibling = newFiber;
303 | }
304 |
305 | prevSibling = newFiber;
306 | index++;
307 | }
308 |
309 | if (fiber.child) {
310 | return fiber.child;
311 | }
312 |
313 | let nextFiber = fiber;
314 |
315 | while (nextFiber) {
316 | if (nextFiber.sibling) {
317 | return newFiber.sibling;
318 | }
319 | nextFiber = nextFiber.parent;
320 | }
321 | }
322 | ```
323 |
324 | ## 5. render and commit phases
325 | 여기서 문제가 있다.
326 |
327 | 우리는 각 element에 대하여 dom에 새 node를 달아주고 있는데, 만약 모든 dom을 그리기전에 브라우저가 제어권을 뺏어간다면 rendering이 끝나기전에 불완전한 UI를 보여주고 말 것이다.
328 |
329 | 그래서 performUnitOfWork에서 dom을 매번 추가하는 부분을 지우고, fiber tree의 root를 wiproot라고 이름 짓고 이를 추적할 것이다.
330 |
331 | ```js
332 | // performUnitOfWork 지울부분
333 | if (fiber.parent) {
334 | fiber.parent.dom.appendChild(fiber.dom);
335 | }
336 | ```
337 |
338 | ``` js
339 | function render(element, container) {
340 | wipRoot = {
341 | dom: container,
342 | props: {
343 | children: [element],
344 | },
345 | };
346 | nextUnitOfWork = wipRoot;
347 | }
348 | ```
349 |
350 | 다음 실행 단위가 없어, 모든 실행 단위가 끝이 나면 비로소 dom에 commit한다. 이를 commitRoot 함수에서 실행한다.
351 |
352 | ``` js
353 | function workLoop(deadline) {
354 | let shouldYield = false;
355 | while (nextUnitOfWork && !shouldYield) {
356 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
357 | shouldYield = deadline.timeRemaining() < 1;
358 | }
359 |
360 | if (!nextUnitOfWork && wipRoot){
361 | commitRoot()
362 | }
363 |
364 | requestIdleCallback(workLoop);
365 | }
366 | ```
367 |
368 | ```js
369 | function commitRoot() {
370 | commitWork(wipRoot.child);
371 | wipRoot = null;
372 | }
373 |
374 | function commitWork(fiber) {
375 | if (!fiber) {
376 | return;
377 | }
378 | const domParent = fiber.parent.dom;
379 | domParent.appendChild(fiber.dom);
380 | commitWork(fiber.child);
381 | commitWork(fiber.sibling);
382 | }
383 | ```
384 | ## 6. reconciliation
385 | 지금까지 dom에 추가하는 기능을 구현했다. 이제는 update와 unmount을 구현해보자.
386 |
387 | 우선 마지막 추가한 마지막 fiber tree를 currentRoot로 저장한다.
388 |
389 | 또한, alternate라는 프로퍼티를 fiber에 추가해 이를 기억하게 한다.
390 | ```js
391 | function render(element, container) {
392 | wipRoot = {
393 | dom: container,
394 | props: {
395 | children: [element],
396 | },
397 | alternate: currentRoot,
398 | };
399 |
400 | nextUnitOfWork = wipRoot;
401 | }
402 | ```
403 |
404 | performUnitOfWork에서 자식 fiber를 그리는 부분을 추출해서 reconcileChildren() 함수로 다시 만든다.
405 |
406 | 이제는 자식 fiber를 그리는 동안 이전 oldFiber와 달라진 점이 없는지 체크를 할 것이다.
407 |
408 | 비교를 위해서는 타입을 사용한다.
409 |
410 | 같은 타입이라면 기존 노드를 사용하고 props만 업데이트 해준다.
411 |
412 | 타입이 다르고 새 element가 있다면 새로운 dom 노드를 생성한다.
413 |
414 | 타입이 다르고 기존 fiber만 있다면 예전 node를 지워야한다.
415 |
416 | 이 과정에서 실제 리액트는 key를 가지고 비교를 한다. (element 배열에서 효과적일 것이다.)
417 |
418 | 이를 effectTag라는 새로운 프로퍼티를 추가해 표시해 둘 것이다.
419 |
420 | props만 업데이트 될 경우에는 UPDATE를, 새 dom 노드 추가는 PLACEMENT 표기를 해두어 commit시에 활용한다.
421 |
422 | 삭제는 DELETION list를 따로 만들어 관리한다.
423 |
424 | ```js
425 | function reconcileChildren(wipFiber, elements) {
426 | let index = 0;
427 | let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
428 | let prevSibling = null;
429 |
430 | while (index < elements.lenth || oldFiber != null) {
431 | const element = elements[index];
432 | let newFiber = null;
433 | const sameType = oldFiber && element && element.type == oldFiber.type;
434 |
435 | if (sameType) {
436 | newFiber = {
437 | type: oldFiber.type,
438 | props: element.props,
439 | dom: oldFiber.dom,
440 | parent: wipFiber,
441 | alternate: oldFiber,
442 | effectTag: "UPDATE",
443 | };
444 | }
445 |
446 | if (element && !sameType) {
447 | newFiber = {
448 | type: element.type,
449 | props: element.props,
450 | dom: null,
451 | parent: wipFiber,
452 | alternate: null,
453 | effectTag: "PLACEMENT",
454 | };
455 | }
456 |
457 | if (oldFiber && !sameType) {
458 | oldFiber.effectTag = "DELETION";
459 | deletions.push(oldFiber);
460 | }
461 |
462 | if (oldFiber) {
463 | oldFiber = oldFiber.sibling;
464 | }
465 |
466 | if (index == 0) {
467 | fiber.child = newFiber;
468 | } else {
469 | prevSibling.sibling = newFiber;
470 | }
471 |
472 | prevSibling = newFiber;
473 | index++;
474 | }
475 | }
476 | ```
477 |
478 | 이제 commitwork 에서 effectTags에 따라 반영한다.
479 | ``` js
480 | function commitRoot() {
481 | deletions.forEach(commitWork);
482 | commitWork(wipRoot.child);
483 | currentRoot = wipRoot;
484 | wipRoot = null;
485 | }
486 |
487 | function commitWork(fiber) {
488 | if (!fiber) {
489 | return;
490 | }
491 |
492 | const domParent = fiber.parent.dom;
493 |
494 | if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
495 | domParent.appendChild(fiber.dom);
496 | } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
497 | updateDom(fiber.dom, fiber.alternate.props, fiber.props);
498 | } else if (fiber.effectTag === "DELETION") {
499 | domParent.removeChild(fiber.dom);
500 | }
501 |
502 | commitWork(fiber.child);
503 | commitWork(fiber.sibling);
504 | }
505 | ```
506 | update tag에 대해서는 기존 propety들을 교체해주는 작업을 해주는 updateDom 함수를 만든다.
507 |
508 | 이벤트 리스터 property는 "on" 으로 시작하는 특수한 케이스로 예외처리해준다.
509 |
510 | ``` js
511 | const isEvent = (key) => key.startsWith("on");
512 | const isProperty = (key) => key !== "children" && !isEvent(key);
513 | const isNew = (prev, next) => (key) => prev[key] !== next[key];
514 | const isGone = (prev, next) => (key) => !(key in next);
515 |
516 | function updateDom(dom, prevProps, nextProps) {
517 | // remove old or changed event listeners
518 | Object.keys(prevProps)
519 | .filter(isEvent)
520 | .filter((key) => {
521 | !(key in nextProps) || isNew(prevProps, nextProps)(key);
522 | })
523 | .forEach((name) => {
524 | const eventType = name.toLowerCase().substring(2);
525 | dom.removeEventListener(eventType, prevProps[name]);
526 | });
527 |
528 | // remove old properties
529 | Object.keys(prevProps)
530 | .filter(isProperty)
531 | .filter(isGone(prevProps, nextProps))
532 | .forEach((name) => {
533 | dom[name] = "";
534 | });
535 |
536 | // add new properties
537 | Object.keys(nextProps)
538 | .filter(isProperty)
539 | .filter(isNew(prevProps, nextProps))
540 | .forEach((name) => {
541 | dom[name] = nextProps[name];
542 | });
543 |
544 | // add new event listeners
545 | Object.keys(prevProps)
546 | .filter(isEvent)
547 | .filter(isNew(prevProps, nextProps))
548 | .forEach((name) => {
549 | const eventType = name.toLowerCase().substring(2);
550 | dom.addEventListener(eventType, prevProps[name]);
551 | });
552 | }
553 | ```
554 |
555 | ## 7. function components
556 | 다음 추가할 기능은 함수형 컴포넌트이다.
557 |
558 | 함수형 컴포넌트의 차이점은, fiber가 dom node(container)를 갖지 않는다는 점이고, childeren props에서 가져오는 것이 아니라 함수에서 리턴한다는 점이다.
559 |
560 | 우리는 fiber의 타입을 체크해서 함수라면 다른 update 함수를 사용하도록 할 것이다.
561 |
562 | updateHostComponent는 기존 함수와 같고, updateFunctionComponent는 함수로 자식을 가져오도록 할것이다.
563 |
564 | ```js
565 | function performUnitOfWork(fiber) {
566 | const isFunctionComponent = fiber.type instanceof Function;
567 | if (isFunctionComponent) {
568 | updateFunctionComponent(fiber);
569 | } else {
570 | updateHostComponent(fiber);
571 | }
572 |
573 | if (fiber.child) {
574 | return fiber.child;
575 | }
576 |
577 | let nextFiber = fiber;
578 |
579 | while (nextFiber) {
580 | if (nextFiber.sibling) {
581 | return newFiber.sibling;
582 | }
583 | nextFiber = nextFiber.parent;
584 | }
585 | }
586 |
587 | function updateFunctionComponent(fiber) {
588 | const children = [fiber.type(fiber.props)];
589 | reconcileChildren(fiber, children);
590 | }
591 |
592 | function updateHostComponent(fiber) {
593 | if (!fiber.dom) {
594 | fiber.dom = createDom(fiber);
595 | }
596 | reconcileChildren(fiber, fiber.props.children);
597 | }
598 | ```
599 |
600 | commitWork에서 바꿔야할 부분이 있다. 부모를 찾는 부분에서는 dom 노드를 가진 fiber를 찾을때까지 찾도록 하는 것이다.
601 |
602 | 삭제하는 부분도 dom 노들를 가진 자식을 찾을때까지 찾도록 해야 한다.
603 |
604 | ```js
605 | function commitWork(fiber) {
606 | if (!fiber) {
607 | return;
608 | }
609 |
610 | let domParentFiber = fiber.parent;
611 | while (!domParentFiber.dom) {
612 | domParentFiber = domParentFiber.parent;
613 | }
614 | const domParent = domParentFiber;
615 |
616 | if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
617 | domParent.appendChild(fiber.dom);
618 | } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
619 | updateDom(fiber.dom, fiber.alternate.props, fiber.props);
620 | } else if (fiber.effectTag === "DELETION") {
621 | commitDeletion(fiber, domParent);
622 | }
623 | commitWork(fiber.child);
624 | commitWork(fiber.sibling);
625 | }
626 |
627 | function commitDeletion(fiber, domParent) {
628 | if (fiber.dom) {
629 | domParent.removeChild(fiber.dom);
630 | } else {
631 | commitDeletion(fiber.child, domParent);
632 | }
633 | }
634 | ```
635 | ## 8. hooks
636 | 마지막으로 함수형 컴포넌트에 state를 추가하자.
637 |
638 | 먼저 useState에 필요한 전역변수를 선언한다.
639 |
640 | useState가 한 컴포넌트에서 여러번 호출되었을 때를 위해 hooks 배열도 만든다.
641 |
642 | ```js
643 | let wipFiber = null
644 | let hookIndex = null
645 |
646 | function updateFunctionComponent(fiber) {
647 | wipFiber = fiber
648 | hookIndex = 0
649 | wipFiber.hooks = []
650 | const children = [fiber.type(fiber.props)];
651 | reconcileChildren(fiber, children);
652 | }
653 | ```
654 |
655 | useState가 호출되면, 이전에 호출된 hook이 있는지 확인한다.
656 |
657 | hook이 있다면 이전 hook에서 state를 복사해 온다.
658 |
659 | 새로운 hook을 fiber에 넣어주고 state를 반환한다.
660 |
661 | useState는 상태를 변환시키기 위한 함수를 반환해야한다.
662 |
663 | 그래서 setState를 선언해 action을 받도록 선언해야하고, 이 action을 hook에 큐에 삽입한다.
664 |
665 | 그 다음은 마치 render가 작동하듯이 차근차근 다음 실행단위를 실행시키도록 한다.
666 |
667 | 이 action들은 우리가 다음 랜더링을 할 때 old hook 에 쌓여있던 action들이 실행되며 실행된다.
668 | ```js
669 | function useState(initial) {
670 | const oldHook =
671 | webfiber.alternate &&
672 | webFiber.alternate.hooks &&
673 | webFiber.alternate.hooks[hookIndex];
674 |
675 | const hook = { status: oldHook ? oldHook.state : initial, queue: [] };
676 |
677 | const action = oldHook ? oldHook.queue : []
678 | action.forEach(action => {
679 | hook.state = action(hook.state)
680 | })
681 |
682 | const setState = (action) => {
683 | hook.queue.push(action);
684 | wipRoot = {
685 | dom: currnetRoot.Dom,
686 | props: currnetRoot.props,
687 | alternate: currnetRoot,
688 | };
689 | (nextUnitOfWork = wipRoot), (deletions = []);
690 | };
691 |
692 | wipFiber.hooks.push(hook);
693 | hookIndex++;
694 | return [hook.state, setState];
695 | }
696 | ```
697 |
--------------------------------------------------------------------------------