[번역] Didact 파이버: 점진적 재조정

Ryan Yang·2019년 3월 30일
4
post-thumbnail

이 스토리는 리액트 DIY 시리즈의 일부입니다. 하지만 전에 작성한 코드 대부분을 재 작성할 것 입니다.

  • 지금까지 시리즈를 요약하자면, 리액트 내부에서 어떤 일이 일어나는지 이해하기 위해 리액트 클론을 작성했습니다. 우리는 그걸 Didact 라고 합니다. 리액트 주요 기능에 집중하기 위해 코드를 간단히 했습니다. 먼저 어떻게 엘리먼트를 랜더링하고 JSX가 작동하는지 알아봤습니다. 업데이트간에 변경된 항목 만 다시 렌더링하기 위해 조정 알고리즘을 작성했습니다. 그리고 클래스 컴포넌트와 setState()를 작성했습니다.

이제 리액트 16이 출시되었고, 새로운 내부 아키텍처로 대부분의 코드를 재 작성해야 합니다.

오랬 동안 기다려왔던 기능(오래된 설계로 인해 개발하기 힘들었던)이며 마침내 정식으로 반영되었습니다.

또한 이 시리즈에서 작성했던 대부분의 코드는 이제 의미가 퇴색하였습니다 😛.

이 포스트에서는 리액트 16의 새로운 아키텍처와 함께 대부분의 코드를 재 작성 해볼 겁니다. 우리는 리액트 코드 베이스에서 구조, 변수, 함수 이름을 그대로 가져올 것 입니다. 그러니 모든 공개 API는 건너뛰도록 하겠습니다.

  • Didact.createElement()
  • Didact.render() (오직 DOM 랜더링)
  • Didact.Component (setState() 와 함께이지만 context와 라이프 사이클 메서드는 제외)

이전에 작동하는 코드를 보려면 업데이트된 데모 로 이동 하거나 깃헙 저장소를 방문하세요.

자 이제, 왜 이전 코드를 다시 작성해야 하는지 설명하겠습니다.

왜 파이버인가?

여기서 리액트 파이버의 전체적인 그림에 대해 설명하지는 않을 겁니다. 만약 이것에 대해 더 알고 싶다면, 리소스들의 목록를 확인하세요.

브라우저 메인 스레드가 어떤 이유로 계속 바쁜 상태일 때, 중요한 태스크는 받아들여지지 못하고 전체가 끝날 때 까지 계속 기다릴 것 입니다.

이 문제를 보여주기 위한 작은 데모를 만들었습니다. 이 데모에서 행성은 계속 돌고 메인 스레드는 매 16ms초 마다 사용 가능해야 합니다. 그래서, 만약 메인 스레드가 다른 것을 하느라 블록 되어 있다면 (200ms 정도라 해보죠), 애니메이션 프레임은 누락될 것이고 행성은 메인 스레드가 풀려날 때까지 얼어 있을 것 입니다.

애니메이션을 부드럽게 유지하고, UI를 반응성 있게 유지하기 위한 몇 마이크로 초를 절약 하지 못하게, 메인 스레드를 너무 바쁘게 하는 것이 무엇인가요?

재조정 코드를 기억하실지 모르겠습니만, 재조정은 한번 시작되면 멈추지 않습니다. 만약 메인스레드가 다른 무엇인가를 하려면 기다려야 하죠. 해당 코드는 재귀적 호출에 의존하고 있어 일시정지하기 힘들기 때문입니다. 그래서 재귀 호출을 루프로 대체 할 수 있는 새로운 데이터 구조를 사용하여 다시 작성 하려고 합니다.

joke.png

트위터 링크

Rodrigo Pombo:
리액트가 어떻게 재귀 없이 파이버 트리를 순회하는지 이해하는데 시간이 좀 필요하... 82
("takes a while"을 이용한 농담) 
정말로 이 농담을 많은 사람들에게 이해시키려고 이 글을 작성했습니다.

마이크로 스케줄링

작업을 더 작은 조각들로 나누고, 짧은 시간 동안 그 조각들을 실행하고, 메인 스레드가 우선 순위가 높은 것들을 수행하게 하고, 대기중인 것이 있으면 작업을 끝내고 되돌아오게 하는 것이 필요할 것 입니다.

우리는 requestIdelCallback() 함수의 도움을 받아 이것을 수행할 겁니다. 다음 코드는 브라우저가 유휴 상태 일 때 콜백이 호출되도록 대기시키며, deadline 인자는 사용 가능한 시간을 나타내는 매개 변수를 포함합니다.

scheduler.js:

const ENOUGH_TIME = 1; // 밀리세컨드

let workQueue = [];
let nextUnitOfWork = null; // 다음 작업 단위

function schedule(task) {
  workQueue.push(task);
  requestIdleCallback(performWork);
}

function performWork(deadline) {
  if (!nextUnitOfWork) {
    nextUnitOfWork = workQueue.shift();
  }

  while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  if (nextUnitOfWork || workQueue.length > 0) {
    requestIdleCallback(performWork);
  }
}

실제 작업은 preformUnitOfWork() 함수 내부에서 발생 합니다. 이곳에 재조정 코드를 작성해야 합니다. 이 함수는 작업의 조각을 실행하고 다음에 작업을 다시 시작하는데 필요한 모든 정보를 반환해야 합니다.

이러한 작업을 추적하기 위해 파이버를 사용할 것 입니다.

파이버 데이터 구조

랜더링 하려는 각 컴포넌트에 대해 파이버를 만들 것 입니다. 이 nextUnitOfWork는 수행하고자 하는 다음 파이버에 대한 참조가 될 것 입니다. performUnitOfWork는 해당 파이버에서 동작하고 모든 작업이 완료될 때 까지 새로운 파이버를 반환합니다. 이 부분은 나중에 자세히 설명 할 테니 일단 저를 믿고 따라와주세요.

파이버는 어떻게 생겼을까요?

didact-fiber.js:

let fiber = {
  tag: HOST_COMPONENT,
  type: "div",
  parent: parentFiber,
  child: childFiber,
  sibling: null,
  alternate: currentFiber,
  stateNode: document.createElement("div"),
  props: { children: [], className: "foo"},
  partialState: null,
  effectTag: PLACEMENT,
  effects: []
};

이것은 평범한 자바스크립트 객체입니다.

컴포넌트들의 트리를 서술하기 위해 parent(부모), child(자식), sibling(형제) 속성을 사용할 것입니다.

stateNode는 컴포넌트 인스턴스에 대한 참조입니다. 이는 DOM 엘리먼트이거나 사용자 정의 클래스 컴포넌트의 인스턴스 일 수 있습니다.

예를들어:
1_tc8Jcye70jRI79dmI4PUcw.png

  • 호스트 컴포넌트들로 대표되는 b, p, i를 위한 파이버들. 이 것들은 tagHOST_COMPONENT로 하여 구분할 것 입니다. 이 파이버들의 type은 문자열(HTML 엘리먼트의 태그)입니다. props는 속성(attributes)과 엘리먼트의 이벤트 리스너(event listener)가 될 수 있습니다.
  • 클래스 컴포넌트로 대표되는 Foo 파이버. 이것들의 tagCLASS_COMPONENT이며 type은 유저가 Didact.Component를 상속하여 정의한 클래스 입니다.
  • 호스트 루트로 대표되는 div를 위한 파이버. 이것은 호스트 컴포넌트와 유사하며, 이것은 stateNode 로써 DOM 엘리먼트지만 트리의 루트이므로 특별한 취급을 받게 됩니다. 이 파이버의 tagHOST_ROOT 입니다. 참고로 파이버의 stateNodeDidact.render()로 넘겨진 DOM 노드입니다.

다른 중요한 속성(property)은 alternate(교체) 입니다. 이것이 필요한 이유는 대부분의 시간에서 두개의 파이버 트리가 필요하기 때문이죠. 하나의 트리는 이미 DOM에 랜더링한 것들과 일치하며, 현재 트리(current tree) 혹은 기존 트리(old tree)라고 부르겠습니다. 다른 하나는 새로운 업데이트(setState() 혹은 Didact.render()를 호출할 때)를 할 때 빌드하는 트리입니다. 이 트리는 작업 중(work-in-progress) 트리라고 합니다

작업 중(work-in-progress) 트리는 기존 트리(old tree)와 어떤 파이버도 공유하지 않습니다. 일단 진행 중 트리를 만들고 DOM을 변이 시키면 작업중인 트리가 기존 트리가 됩니다.

따라서 alternate(교체)는 작업중인 파이버를 기존 파이버로부터 상응하는 파이버를 연결하는데 사용합니다. 파이버와 alternate(교체)는 같은 tag, type 그리고 stateNode를 공유합니다. 때로는(새로운 것을 랜더링 할 때) 파이버가 alternate를 가지지 않을 것 입니다.

마지막으로, effect 리스트와 effectTag를 가지고 있습니다. 작업 중(work-in-progress) 트리에서 DOM 변경이 필요한 파이버가 발견되면 effectTag를 PLACEMENT, UPDATE 혹은 DELETION로 설정합니다. 모든 DOM 변경을 함께 처리하기 쉽도록 하기 위해 모든 파이버의 목록(파이버 하위 트리로부터)인 effectTag 목록을 effects에 유지합니다.

곧 파이버 트리가 어떤 일을 하는지 살펴볼 것 이니, 한번에 너무 많은 정보가 쏟아져, 따라오지 못할 것을 걱정 하진 않으셔도 됩니다.

Didact 호출 계층

작성하려고 하는 코드의 흐름을 이해하기 위해 아래 다이어그램을 살펴보죠.

1_dZtg4-sSt_8FlmFWArAxkA.png

우린 render()setState()에서 시작해 commitAllWork()에서 끝나는 흐름을 따릅니다.

이전 코드

전에 말했다시피 대부분의 코드를 재 작성하려 합니다만, 먼저 재 작성하지 않을 코드가 있는지 한번 살펴봅시다.

엘리먼트 생성과 JSX 포스트에서는 트랜스파일된 JSX가 사용하는 함수인 createElement 함수를 작성했습니다. 이 코드는 변경할 필요가 없으며, 동일한 엘리먼트들을 계속 사용할 것 입니다. 엘리먼트들의 type, props, children에 대해 알지 못한다면, 이전 포스트를 참조하세요.

인스턴스, 재조정과 가상 DOM에서는 노드의 DOM 프로퍼티 갱신을 위하여 updateDomProperties() 함수를 작성했습니다. 또한 DOM 엘리먼트를 만들기 위한 createDomElement() 코드를 추출했습니다. 이 두 함수 모두 dom-utils.js gist에서 보실 수 있습니다.

컴포넌트와 상태에서는 클래스에 기반한 Component(컴포넌트)를 작성했습니다. 자 그럼 setState()scheduleUpdate()를 호출하고, createInstance()가 파이버 인스턴스의 참조를 저장하도록 변경해봅시다.

component.js:

class Component {
  constructor(props) {
    this.props = props || {};
    this.state = this.state || {};
  }

  setState(partialState) {
    scheduleUpdate(this, partialState);
  }
}

function createInstance(fiber) {
  const instance = new fiber.type(fiber.props);
  instance.__fiber = fiber;
  return instance;
}

이 정도로 해두고 나머지 코드는 처음부터 다시 작성하지 않습니다.


1_axF-r9RCJPFYiCbyHActAA.png

Component(컴포넌트) 클래스와 createElement() 외에도 render()setState()라는 두 공개(public) 함수들이 있는데, setState()는 그저 scheduleUpdate()를 호출하는 것을 앞에서 보았습니다.

render()scheduleUpdate()는 유사한데, 이 함수들은 새로운 업데이트를 받아 그것을 큐에 쌓아 둡니다.

didact-fiber.js

// 파이버 태그들
const HOST_COMPONENT = "host";
const CLASS_COMPONENT = "class";
const HOST_ROOT = "root";

// 전역 상태
const updateQueue = [];
let nextUnitOfWork = null; // 다음작업단위
let pendingCommit = null;

function render(elements, containerDom) {
  updateQueue.push({
    from: HOST_ROOT,
    dom: containerDom,
    newProps: { children: elements }
  });
  requestIdleCallback(performWork);
}

function scheduleUpdate(instance, partialState) {
  updateQueue.push({
    from: CLASS_COMPONENT,
    instance: instance,
    partialState: partialState
  });
  requestIdleCallback(performWork);
}

우린 updateQueue 배열을 사용하여 보류중인 업데이트를 추적할 것 입니다. render() 혹은 scheduleUpdate()가 매번 호출할 때 마다 새로운 업데이트를 updateQueue 밀어 넣습니다. 각 업데이트들의 업데이트 정보는 다르며 resetNextUnitOfWork()에서 이것들을 어떻게 사용하는지 사용 하는지 차차 살펴볼 것 입니다.

업데이트를 큐에 밀어 넣은 뒤 performWork()에 대한 지연 호출을 격발(trigger)시킵니다.

1_uDJSf8E4fU87701Fb4bmdQ.png

didact-fiber.js

const ENOUGH_TIME = 1; // milliseconds

function performWork(deadline) {
  workLoop(deadline);
  if (nextUnitOfWork || updateQueue.length > 0) {
    requestIdleCallback(performWork);
  }
}

function workLoop(deadline) {
  if (!nextUnitOfWork) {
    resetNextUnitOfWork();
  }
  while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  if (pendingCommit) {
    commitAllWork(pendingCommit);
  }
}

자 이전에 보았던 performUnitOfWork() 패턴을 사용합니다.

requestIdleCallback()은 기한(deadline)을 인수로 하여 대상 함수를 호출합니다. performWork()는 기한(deadline)을 받아 workLoop()로 넘겨줍니다. workLoop()가 반환된 이후, performWork()는 보류중인 작업이 있는지 확인합니다. 존재하는 경우, 새로운 지연 호출을 자체적으로 스케줄합니다.

workLoop()는 시간을 주시하는 함수입니다. 마감 시간이 너무 가까우면 작업 루프를 중지하고 다음 번에 다시 시작할 수 있게 nextUnitOfWork를 업데이트된 채로 남겨둡니다.

deadline.timeReamining()이 다른 작업 단위를 실행하기에 충분한지 아닌지를 확인하기 위해 ENOUGH_TIME(1ms 상수, React와 동일)을 사용합니다. performUnitOfWork()이 그 이상이 소요는 경우 기한(deadline)을 넘길 것입니다. 기한(deadline)은 브라우저의 제안 일 뿐이므로 몇 밀리 초 정도 초과하여 실행되는 것은 그렇게 나쁘지 않습니다.

performUnitOfWork()는 작업중인 업데이트에 대한 작업 중(work-in-progress) 트리를 작성하고 DOM에 적용해야 하는 변경 사항을 확인합니다. 이는 한번에 한 파이버 씩, 점진적으로 수행될 것 입니다.

performUnitOfWork()가 현재 업데이트를 위한 작업을 모두 끝마치면, null을 반환하고 빠져나와 DOM에 보류중인 변경 사항들을 pendingCommit에 남겨둡니다. 마침내 commitAllWork()pendingCommit으로부터 effects를 받아 DOM을 변경합니다.

commitAllWork()는 루프의 외부에서 호출됩니다. performUnitOfWork() 에서 수행된 작업은 DOM을 변경하지 않으므로 분할하는 것이 좋습니다. 반면, commitAllWork()는 DOM을 변형시키므로 일치하지 않는 UI를 피하기 위하여 한번에 전부 완료되어야 합니다.

우린 여전히 nextUnitOfWork가 어디서부터 왔는지 보지 못했습니다.

1_G2NLlU1jgdIfJu_s7CU0oA.png
*resetNextUnitOfWork()여야 합니다
역자) 그림에 실수가 있었던 듯 합니다

아래는 업데이트를 받아서 초기 nextUnitOfWork로 변환하는 resetNextUnitOfWork() 함수입니다.

didact-fiber.js

// 다음 작업 단위 재설정
function resetNextUnitOfWork() {
  const update = updateQueue.shift();
  if (!update) {
    return;
  }

  // 업데이트 페이로드에서 해당 파이버로 setState 매개 변수 복사
  if (update.partialState) {
    update.instance.__fiber.partialState = update.partialState;
  }

  const root =
    update.from == HOST_ROOT
      ? update.dom._rootContainerFiber
      : getRoot(update.instance.__fiber);

  nextUnitOfWork = {
    tag: HOST_ROOT,
    stateNode: update.dom || root.stateNode,
    props: update.newProps || root.props,
    alternate: root
  };
}

function getRoot(fiber) {
  let node = fiber;
  while (node.parent) {
    node = node.parent;
  }
  return node;
}

resetNextUnitOfWork()는 큐 로부터 최초 업데이트를 풀링하여 시작합니다.

업데이트에 partialState(부분 상태)가 있는 경우 컴포넌트 인스턴스에 속하는 파이버에 이를 저장하므로, 추후 컴포넌트의 render()를 호출할 때 사용할 수 있습니다.

그리고 기존 파이버 트리의 루트를 찾습니다. 만약 최초로 render()가 호출되어 업데이트가 발생하면, root 파이버를 가지고 있지 않기 때문에 null이 될 것 입니다. 만약 후속 render() 호출에서 온 것이라면, DOM 노드의 _rootContainerFiber 속성(property)에서 루트를 찾을 수 있습니다. 그리고 업데이트가 setState()로부터 온 것이라면, 부모(parent)가 없는 파이버를 찾을 때까지 상위 파이버 인스턴스로 올라가야 합니다.

그리고 새로운 파이버 nextUnitOfWork를 할당합니다. 이 파이버는 새로운 작업 중 (work-in-progress) 트리의 루트입니다.

만약 기존 루트가 없는 경우 stateNoderender() 호출에서 인자로 받았던 DOM 노드가 될 것 입니다. props는 업데이트로부터 온 newProps(render() 함수의 또 다른 인자인 엘리먼트의 자식(children) 속성을 가지는 객체)가 될 것입니다. alternate(교체)는 null이 됩니다.

만약 기존 루트가 있는 경우, 이전 루트로부터 온 DOM 노드가 stateNode가 됩니다. propsnewPropsnull이 아니면 취하고, 그렇지 않으면 기존 루트에서 props를 복사합니다.alternate는 기존 루트가 됩니다.

이제 작업 중(work-in-progress) 트리의 루트를 가졌으니, 나머지 부분을 만들기 시작해 봅시다.

1_T9vp_ugVrWc3cEPdskNb3w.png

didact-fiber.js

function performUnitOfWork(wipFiber) {
  beginWork(wipFiber);
  if (wipFiber.child) {
    return wipFiber.child;
  }

  // 자식이 없는 경우 형제(sibling)를 찾기 전까지 completeWork를 호출합니다.
  let uow = wipFiber;
  while (uow) {
    completeWork(uow);
    if (uow.sibling) {
      // Sibling needs to beginWork
      return uow.sibling;
    }
    uow = uow.parent;
  }
}

performUnitOfWork()는 진행 중(work-in-progress) 트리를 순회합니다.

beginWork()를 호출(파이버의 새로운 자식을 생성하기 위해)하고 첫번째 자식(child)을 반환하여 nextUnitOfWork에 대입합니다.

만약 어떠한 자식도 없는 경우 completeWork()를 호출하고 형제(sibling)를 nextUnitOfWork로써 반환합니다.

만약 어떠한 형제(sibling)도 없는 경우 형제(sibling)(nextUnitOfWork가 될)을 찾거나 루트에 도달할 때 까지 completeWork()를 호출하여 부모로 올라가는 것을 반복합니다.

performUnitOfWork()를 여러 번 호출하면 자식(children)이 없는 파이버를 찾을 때까지 각 파이버의 첫번째 자식의 자식들을 생성하면서 계속해서 트리의 하위로 내려갑니다. 그런 다음 이웃에 대해서도 동일한 작업을 수행합니다. 그리고 상위로 올라가 삼촌에 대해서도 같은 작업을 수행합니다. (좀 더 생생한 설명을 원한다면 파이버 디버거에서 일부 컴포넌트를 랜더링 해보세요)

1_YzayoR7QCM3b77tOiEO23w.png

didact-fiber.js

function beginWork(wipFiber) {
  if (wipFiber.tag == CLASS_COMPONENT) {
    updateClassComponent(wipFiber);
  } else {
    updateHostComponent(wipFiber);
  }
}

function updateHostComponent(wipFiber) {
  if (!wipFiber.stateNode) {
    wipFiber.stateNode = createDomElement(wipFiber);
  }
  const newChildElements = wipFiber.props.children;
  reconcileChildrenArray(wipFiber, newChildElements);
}

function updateClassComponent(wipFiber) {
  let instance = wipFiber.stateNode;
  if (instance == null) {
    // 클래스 생성자 호출
    instance = wipFiber.stateNode = createInstance(wipFiber);
  } else if (wipFiber.props == instance.props && !wipFiber.partialState) {
    // 지난번에 복제한 자식을, 랜더링 할 필요가 없습니다.
    cloneChildFibers(wipFiber);
    return;
  }

  instance.props = wipFiber.props;
  instance.state = Object.assign({}, instance.state, wipFiber.partialState);
  wipFiber.partialState = null;

  const newChildElements = wipFiber.stateNode.render();
  reconcileChildrenArray(wipFiber, newChildElements);
}

beginWork()는 두가지를 수행합니다.

  • stateNode가 없다면 생성합니다.
  • 컴포넌트 자식들을 가져와 reconcileChildrenArray()로 넘겨줍니다.

컴포넌트의 타입에 따라 다르므로 updateHostComponent()updateClassComponent() 둘로 나눠 다를 것 입니다.

updateHostComponent()는 호스트 컴포넌트들과 루트 컴포넌트를 다룹니다. 이는 필요한 경우 새로운 DOM 노드를 생성합니다 (자식 혹은 DOM에 이어 붙이는 것 없이, 오직 단일 노드). 그런 다음 파이버 props의 자식 앨리먼트를 사용하여 reconcileChildrenArray()를 호출합니다.

updateClassComponent()는 클래스 컴포넌트 인스턴스를 처리합니다. 필요한 경우 컴포넌트 생성자를 호출하여 새 인스턴스를 생성합니다. 이 인스턴스의 propsstate가 업데이트되면 render() 함수를 호출하여 새로운 자식들을 얻을 수 있습니다.

updateClassComponent()는 또한 render()를 호출하는 것이 타당한지 검증합니다. 이것은 shouldComponentUpdate()의 간단한 버전입니다. 만약 다시 랜더링 할 필요가 없는 것처럼 보이면 조정 없이 현재 서브 트리를 진행 중(work-in-progress) 트리에 복제(clone)합니다.

이제 newChildElements를 가지고, 진행 중(work-in-progress) 파이버를 위한 자식 파이버를 생성할 준비가 되었습니다.

1_PBfEr0xOp6AxpyaCkNPtcQ.png

이것이 진행 중 (work-in-progress) 트리가 성장하고, 커밋 단계에서 DOM에 어떤 변화를 줄 것인지 결정하는, 라이브러리의 심장입니다.

didact-fiber.js

// Effect tags
const PLACEMENT = 1;
const DELETION = 2;
const UPDATE = 3;

function arrify(val) {
  return val == null ? [] : Array.isArray(val) ? val : [val];
}

function reconcileChildrenArray(wipFiber, newChildElements) {
  const elements = arrify(newChildElements);

  let index = 0;
  let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
  let newFiber = null;
  while (index < elements.length || oldFiber != null) {
    const prevFiber = newFiber;
    const element = index < elements.length && elements[index];
    const sameType = oldFiber && element && element.type == oldFiber.type;

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        tag: oldFiber.tag,
        stateNode: oldFiber.stateNode,
        props: element.props,
        parent: wipFiber,
        alternate: oldFiber,
        partialState: oldFiber.partialState,
        effectTag: UPDATE
      };
    }

    if (element && !sameType) {
      newFiber = {
        type: element.type,
        tag:
          typeof element.type === "string" ? HOST_COMPONENT : CLASS_COMPONENT,
        props: element.props,
        parent: wipFiber,
        effectTag: PLACEMENT
      };
    }

    if (oldFiber && !sameType) {
      oldFiber.effectTag = DELETION;
      wipFiber.effects = wipFiber.effects || [];
      wipFiber.effects.push(oldFiber);
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (index == 0) {
      wipFiber.child = newFiber;
    } else if (prevFiber && element) {
      prevFiber.sibling = newFiber;
    }

    index++;
  }
}

시작하기 전에 newChildElements가 배열이라는 사실을 기억하시길 바랍니다. (이전의 조정 알고리즘과 달리, 이번 알고리즘은 자식들의 배열로 동작하는데, 이는 컴포넌트의 render() 함수가 배열을 반환할 수 있다는 의미입니다)

그런 다음 기존 파이버 트리의 자식들과 새로운 엘리먼트들(우리는 파이버를 엘리먼트와 비교합니다)을 비교합니다. 기존 파이버 트리의 자식들은 wipFiber.alternate의 자식들입니다. 새로운 엘리먼트들은 wipFiber.stateNode.render()를 호출하거나 wipFiber.props.children로부터 얻은 것 입니다.

재조정 알고리즘은 기존 파이버(wipFiber.alternate.child)와 첫번째 자식 엘리먼트(elements[0])를 일치시키고, 두번째 기존 파이버(wipFiber.alternate.child.sibling)은 두번째 자식 엘리먼트(elements[1])와 일치시키는 것을 반복하여 동작합니다. 각각 oldFiber - element 쌍

역자주: line 16 반복 문에서 엘리먼트나 기존 파이버가 없을 때까지 반복
  • 만약 oldFiberelement가 같은 type을 가진다면, 이는 좋은 소식으로 기존 stateNode를 계속해서 사용할 수 있다는 의미입니다. 기존의 것으로부터 새로운 파이버를 생성합니다. UPDATE effectTag도 추가해줍니다. 그리고 새로운 파이버를 진행 중 (work-in-progress) 트리에 덧붙입니다.
  • 만약 elementtypeoldFiber와 다르거나 oldFiber가 없다(기존 자식들보다 새로운 자식들을 많이 가지고 있는 경우)면, 우리가 가지고 있는 element에 있는 정보로 새로운 파이버를 생성합니다. 이 새로운 파이버는 alternatestateNode(stateNodebeginWork() 에서 생성할 것)가 없습니다. 이 파이버의 effectTagPLACEMENT 입니다.
  • 만약 oldFiberelement가 다른 type 혹은 이 oldFiber를 위한 어떠한 element도 없는 경우(왜냐하면 기존 자식들이 새로운 자식들을 많이 가지고 있기 때문) oldFiber를 위해 DELETION 태그를 붙입니다. 이 파이버는 작업 중(work-in-progress) 트리의 일부가 아니기 때문에, 그것을 추적할 수 없도록 wipFiber.effets 목록에 추가해야 합니다.

리액트와는 달리 재조정을 위해 keys를 사용하지 않으므로, 이전 위치에서 벗어난 자식이 있는지 알 수 없습니다.

1_EKR5c8JGXamBM5G5xzCk1A.png

updateClassComponent()는 재조정을 하는 대신 지름길로 기존 파이버 하위 트리를 진행 중(work-in-progress) 트리로 복제하는 특별한 경우가 있습니다.

didact-fiber.js

function cloneChildFibers(parentFiber) {
  const oldFiber = parentFiber.alternate;
  if (!oldFiber.child) {
    return;
  }

  let oldChild = oldFiber.child;
  let prevChild = null;
  while (oldChild) {
    const newChild = {
      type: oldChild.type,
      tag: oldChild.tag,
      stateNode: oldChild.stateNode,
      props: oldChild.props,
      partialState: oldChild.partialState,
      alternate: oldChild,
      parent: parentFiber
    };
    if (prevChild) {
      prevChild.sibling = newChild;
    } else {
      parentFiber.child = newChild;
    }
    prevChild = newChild;
    oldChild = oldChild.sibling;
  }
}

cloneChildFibers() 는 각 wipFiber.alternate 자식들(children)을 복제하고 진행 중(work-in-progress) 트리에 추가합니다. 아무것도 변경하지 않아도 되므로 어떠한 effectTag도 추가할 필요가 없습니다.

1_6YautmtfMnxyvTUcmVHJtQ.png

performUnitOfWork()에서 wipFiber가 새로운 자식들(children)을 가지고 있지 않거나 이미 모든 자식들이 이미 작업을 완료 했을 때, completeWork()를 호출합니다.

didact-fiber.js

function completeWork(fiber) {
  if (fiber.tag == CLASS_COMPONENT) {
    fiber.stateNode.__fiber = fiber;
  }

  if (fiber.parent) {
    const childEffects = fiber.effects || [];
    const thisEffect = fiber.effectTag != null ? [fiber] : [];
    const parentEffects = fiber.parent.effects || [];
    fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
  } else {
    pendingCommit = fiber;
  }
}

completeWork()는 먼저 클래스 컴포넌트의 인스턴스와 관련된 파이버에 대한 참조를 업데이트합니다. (솔직히 말해서, 여기 있을 필요는 없지만 어딘가에 있어야 합니다)

그런 다음 effects 목록을 작성합니다. 이 목록에는 effectTag가 있는 진행 중(work-in-progress) 서브 트리의 모든 파이버들이 포함됩니다. (DELETION effectTag를 가진 이전 하위 트리의 파이버도 포함). 아이디어는 effectTag가 있는 모든 섬유를 루트 effects 목록에 누적하는 것입니다.

마지막으로 파이버에 부모(parent)가 없다면, 진행 중(work-in-progress) 트리의 루트에 있는 것 입니다. 따라서 우리는 업데이트에 대한 모든 작업을 완료하고 모든 effects를 수집했습니다. workLoop()commitAllWork()를 호출 할 수 있도록 pendingCommit에 루트를 대입합니다.

1_izGLw0xSkSAQstIQAf193A.png

마지막으로 우리가 해야 할 일은 DOM을 변경하는 것 입니다.

didact-fiber.js

function commitAllWork(fiber) {
  fiber.effects.forEach(f => {
    commitWork(f);
  });
  fiber.stateNode._rootContainerFiber = fiber;
  nextUnitOfWork = null;
  pendingCommit = null;
}

function commitWork(fiber) {
  if (fiber.tag == HOST_ROOT) {
    return;
  }

  let domParentFiber = fiber.parent;
  while (domParentFiber.tag == CLASS_COMPONENT) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.stateNode;

  if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) {
    domParent.appendChild(fiber.stateNode);
  } else if (fiber.effectTag == UPDATE) {
    updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag == DELETION) {
    commitDeletion(fiber, domParent);
  }
}

function commitDeletion(fiber, domParent) {
  let node = fiber;
  while (true) {
    if (node.tag == CLASS_COMPONENT) {
      node = node.child;
      continue;
    }
    domParent.removeChild(node.stateNode);
    while (node != fiber && !node.sibling) {
      node = node.parent;
    }
    if (node == fiber) {
      return;
    }
    node = node.sibling;
  }
}

commitAllWork() 먼저 각각의 모든 루트 effects를 순회하여 commitWork() 반복해서 호출합니다. commitWork()는 각 섬유의 effectTag를 검사합니다.

  • PLACEMENT 인 경우 우리는 부모 DOM 노드를 찾은 다음 단순히 파이버의 stateNode를 추가합니다.
  • UPDATE 인 경우 stateNode를 이전 props 및 새 props와 함께 전달하고 updateDomProperties()가 업데이트 할 항목을 결정하도록 합니다.
  • DELETION 이고 파이버가 호스트 컴포넌트인 경우 간단합니다. 그저 removeChild()를 호출하면 됩니다. 그러나 파이버가가 클래스 컴포넌트인 경우 removeChild()를 호출하기 전에 파이버 하위 트리에서 모든 호스트 컴포넌트를 찾아서 제거해야 합니다.

모든 effects가 끝나면 nextUnitOfWorkpendingCommit을 초기화 할 수 있습니다. 작업 진행 중(work-in-progress) 트리는 작업중인 트리가 아닌 이전 트리가 되므로 루트를 _rootContainerFiber에 할당합니다. 이제 우리는 현재의 업데이트가 끝냈고 다음 업데이트를 시작할 준비가 되었습니다.


Didact 실행하기

노출된 공개 API를 한자리에 두고 봤을 때 아래와 같을 것 입니다.

didact-demo.js

function importDidact() {
  // ...
  // All the code we wrote
  // ...

  return {
    createElement,
    render,
    Component
  };
}

/** @jsx Didact.createElement */
const Didact = importDidact();

class HelloMessage extends Didact.Component {
  render() {
    return <div>Hello {this.props.name}</div>;
  }
}

Didact.render(
  <HelloMessage name="John" />,
  document.getElementById("container")
);

혹은 기존 포스트로로부터 업데이트된 버전의 데모를 실행할 수 있습니다. 여기 모든 코드는 didact 저장소npm 에서 사용 가능합니다.

다음은 무엇인가요?

Didact는 리액트의 많은 기능들을 가지고 있지 않으나, 저는 우선도에 따른 업데이트 스케줄링에 특히 관심을 가지고 있습니다.

ReactPriorityLevel.js

module.exports = {
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  TaskPriority: 2, // Completes at the end of the current tick.
  HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4, // Data fetching, or result from updating stores.
  OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};

따라서 다음 게시물은 위의 주제에 대해 다루게 될 수도 있습니다.

이게 전부입니다! 마음에 들었다면 박수 👏, 트위터 팔로우, 코멘트 남기는 것을 잊지 말아주세요

읽어주셔서 감사합니다.


저는 Hexacta에서 여러가지를 만들고있습니다.
아이디어가 있거나 저희를 도울 수 있다면 연락주세요.

출처, 소스코드

0개의 댓글