❄️ [URFC] 02. A first look at react-reconciler

tags
react
URFC
type
Post
summary
status
Published
slug
urfc-02-a-first-look-at-react-reconciler
date
Dec 3, 2023

What is Reconciler?

💡
The Reconciler is the module where the core logic of React resides.
Before we begin this topic, let's first recall how jQuery works:
notion image
jQuery is event-driven, allowing users to indirectly interact with the Host Environment's API (such as the browser's) by calling encapsulated methods.
However, React (and vue) is declarative and state-driven:
notion image
So what the reconciler does is:
  • Consume JSX (or template in vue)
  • Provide a general API for use in different host environments.

How does reconciler consume JSX?

1. The data structure operated on by the reconciler

In the previous article, we mainly introduced the ReactElement data structure. However, using it as the data structure for reconciler operations presents some issues:
  • It cannot express the relationship between nodes.
  • It has limited fields, making it difficult to extend (for instance, it can't express state).
Therefore, a new data structure is needed, characterized by:
  • Being intermediate between ReactElement and real UI nodes.
  • Capable of expressing relationships between nodes.
  • Easily extendable (serving not only as a data storage unit but also as a work unit).
This is the FiberNode (the implementation of Virtual DOM in React).

2. The working method of the reconciler

Compare the ReactElement with the FiberNode for the same node and generating child FiberNodes. Based on the results of the comparison, different flags are generated (insert, delete, move, etc.), corresponding to the execution of different Host Environment APIs.
notion image
For example, mounting <div></div> :
// React Element <div></div> jsx("div") // check corresponding fiberNode null // generate child fiberNode... // Flags: Placement
Update <div></div> to <p></p>
// React Element <p></p> jsx("p") // check corresponding fiberNode FiberNode {type: 'div'} // generate child fiberNode... // Flags: Deletion Placement
After all ReactElement are compared, a FiberNode tree is generated. There are two FiberNode trees in existence:
  • The Current Fiber Tree: This tree represents the UI state currently displayed on the page. It reflects the state last rendered and committed to the DOM, which is the interface users currently see.
  • The Work-In-Progress (WIP) Fiber Tree: This tree represents the state of ongoing work. When state or props change, React works on this tree to compute the new UI. Once this work is completed, it becomes the new Current Fiber Tree.
The purpose of this dual-tree architecture is to facilitate React's Time Slicing and Concurrent Mode functionalities. Time Slicing allows React to pause, interrupt, or resume work during the rendering process, meaning React can suspend rendering tasks when the browser needs to handle more urgent tasks, such as processing user inputs. Concurrent Mode enables React to handle multiple tasks during rendering, which helps improve responsiveness and performance in large applications.

3. How to go through the ReactElement

The answer is DFS (Depth First Search), which means:
  • If there are child nodes, traverse the child nodes.
  • If there are no child nodes, traverse the sibling nodes.

Coding Time

1. Define Types

// react-reconciler/src/fiberFlags.ts export type Flags = number; export const NoFlags = 0b000001; export const Placement = 0b000010; export const Update = 0b000100; export const ChildDeletion = 0b001000;
// react-reconciler/src/workTags.ts export type WorkTag = | typeof FunctionComponent | typeof HostRoot | typeof HostComponent | typeof HostText; export const FunctionComponent = 0; // function App() {} export const HostRoot = 3; // ReactDOM.render export const HostComponent = 5; // <div> export const HostText = 6; // text node "hello"

2. FiberNode Class

If you're not quite clear about the function of some of these fields, that's okay. We will encounter them later on.
import { Props, Key, Ref } from "shared/ReactType"; import { WorkTag } from "./workTags"; import { Flags, NoFlags } from "./fiberFlags"; export class FiberNode { tag: WorkTag; key: Key; stateNode: any; type: any; ref: Ref; return: FiberNode | null; sibling: FiberNode | null; child: FiberNode | null; index: number; pendingProps: Props; memoizedProps: Props | null; alternate: FiberNode | null; flags: Flags; constructor(tag: WorkTag, pendingProps: Props, key: Key) { // As attributes of static data structures this.tag = tag; // the type of component: Function/Class/Host... this.key = key; // key of this fiber this.stateNode = null; // the real DOM node corresponding to Fiber // for FunctionComponent, type refers to the function itself; // for ClassComponent, type refers to the class; // for HostComponent, type refers to the DOM node tagName; this.type = null; // Tree Structure this.return = null; // parent fiber node this.sibling = null; // the first sibling fiber node on the right this.child = null; // first child fiber node this.index = 0; // index of this fiber in the parent's children this.ref = null; // ref of this fiber // Attributes as a dynamic unit of work this.pendingProps = pendingProps; // start with the incoming props this.memoizedProps = null; // props after reconciliation // Point to the fiber corresponding to the fiber in another update // Wether is the current fiber or the work-in-progress fiber // currentFiber.alternate === workInProgressFiber; // workInProgressFiber.alternate === currentFiber; this.alternate = null; // Effects this.flags = NoFlags; // flags to indicate the lifecycle of this fiber } }

3. beginWork() and completeWork()

We need two methods to be invoked during the two stages of DFS:
  • forward: beginWork()
  • backward: completeWork()
notion image
The primary role of beginWork is to start processing a fiber node and completeWork is responsible for completing the processing of a fiber node.
💡
We would implement these two method and discuss the details in the the following articles. For now, just leave them here.
// react-reconciler/src/beginWork.ts export const beginWork = (fiber: FiberNode) => { // do something... return null; // for now, just return null };
// react-reconciler/src/completeWork.ts export const completeWork = (fiber: FiberNode) => { // do something... };

4. workLoop

As previously mentioned, the main task here is to traverse the entire FiberNode Tree using DFS from the root node, and process all nodes by calling beginWork() and completeWork().
// react-reconciler/src/workLoop.ts import { beginWork } from "./beginWork"; import { completeWork } from "./completeWork"; import { FiberNode } from "./fiber"; // Track the FiberNode that is currently being processed let workInProgress: FiberNode | null = null; // Initialize the workInProgress function prepareFreshStack(fiber: FiberNode) { workInProgress = fiber; } // Render the root FiberNode function renderRoot(root: FiberNode) { // Initialize prepareFreshStack(root); do { try { // Work Loop workLoop(); break; } catch (err) { console.warn("Error in the work loop: ", err); workInProgress = null; } // eslint-disable-next-line no-constant-condition } while (true); } function workLoop() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } } // Forward stage in dfs function performUnitOfWork(fiber: FiberNode) { // If beginWork returns a child node (next), this means that // more work needs to be done in the subtree. // At this point, the workInProgress is updated to this child node, // actually moving down the tree (DFS), looking for the next node to process. const next = beginWork(fiber); fiber.memoizedProps = fiber.pendingProps; if (next === null) { completeUnitOfWork(fiber); } else { workInProgress = next; } } // Backward stage in dfs function completeUnitOfWork(fiber: FiberNode) { // Attempt to complete the current unit of work, then move to the next // sibling. If there are no more siblings, return to the parent fiber. // It traverses the sibling node and the parent node until it returns // to the root node. let node: FiberNode | null = fiber; do { completeWork(node); const sibling = node.sibling; if (sibling !== null) { workInProgress = sibling; return; } node = node.return; workInProgress = node; } while (node !== null); }

© Stimw 2022 - 2024