The purpose is to get to know Fiber and implement basic functions of React. Please read this article with the following questions in mind.

  • What are the pain points of Act15? What is Fiber? Why did Act16 need Fiber?
  • How to implement the virtual DOM under Act16?
  • How to implement Fiber data structure and traversal calculation method?
  • How to realize interruptible and recoverable task scheduling in Fiber architecture?
    • How do I specify quantity updates? How to batch update?
  • How to implement component rendering and side effect collection submission in Fiber architecture?
  • How to implement harmonic and double buffering optimization strategies in Fiber?
  • How to implement Hooks such as useReducer and useState?
  • How to implement the expirationTime task priority task scheduling timeout processing?
  • How to optimize key processing for Reconcile Domdiff?
  • How to implement a SyntheticEvent?
  • How to implement ref useEffect?

This article was first published on@careteen/react, please indicate the source. The repository holds all the implementation code and examples, and you can fork if you are interested.

directory

  • React15 scheduling policy
  • Browser task scheduling policy and rendering process
  • Advantages of linked lists
    • Simulation setState
  • The Fiber architecture
    • What did you do before Fiber
      • The React15 DOMDIFF
    • What is the Fiber
      • Fiber is an execution unit
      • Fiber is also a data structure
      • Fiber summary
    • Fiber Execution Phase
    • Reconciliation phase
    • The Commit phase
  • The React with Fiber
    • Prepare the environment
    • Implement the createElement method
    • Implementing the first render
    • Render summary
    • Implement updates to elements
      • Double buffered update policy
    • Implementation class component
    • Implement functional components
    • Realize the Hooks
  • The resources

React15 scheduling policy

JavaScript is like a one-way street.

JavaScript is run single-threaded. In a browser environment, he is responsible for JavaScript parsing and execution of the page, drawing, event handling, and static resource loading and processing. In addition, only one task can be executed. If one task takes a long time, subsequent tasks cannot be executed, and the browser will be stuck.

React15’s rendering and diff recursively compare the VirtualDOM tree to find the nodes that have been added or deleted, and then update them synchronously, all in one go. React will hog browser resources if the number of nodes on the page is very large. Either events triggered by the user will not be responded to, or frames will drop. The user will feel the lag.

So for the above pain points, we expect to find the nodes that have additions, delets, and changes, and then update them synchronously and break the process into two separate parts, or somehow make the whole process interruptible and recoverable, similar to the uniprocessor scheduling of a multitasking operating system.

In order to realize the concurrency of processes, the operating system will allocate the EXECUTION right of CPU to multiple processes according to a certain scheduling policy. Multiple processes have the opportunity to be executed, so that they can be alternately executed to form the illusion of running at the same time. Because the CPU is too fast for a human to feel. In fact, only one program is running at a time in a single-core physical environment.

Browser task scheduling policy and rendering process

Playing games requires a smooth refresh rate, which is at least 60 Hertz. Otherwise the game experience is terrible.

So what does a frame contain?

The average frame is 16.66ms, which is mainly divided into the following parts

  • Script execution
  • Style calculation
  • layout
  • redraw
  • synthetic

The callback used in the script calculation to requestAnimationFrame is executed before the style calculation

If you don’t already know about requestAnimationFrame, head over to MDN to see an example of the progress bar implementation.

After composition, there is an idle phase where composition and all previous steps take less than 16.66ms, and the rest of the time the browser provides us with requestIdleCallback calls to make full use of it.

RequestIdleCallback currently only supports Chrome and requires Polyfill

The general process is as follows:

RequestIdleCallback sample

RequestIdleCallback enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses.

Advantages of linked lists

Because the size of an array is fixed, inserting or removing items from the beginning or middle of the array is expensive. The advantage of linked lists over traditional arrays is that there is no need to move other elements when adding or removing elements. When many elements need to be added or removed, the best choice is a linked list rather than an array. Linked lists play a big role in the Fiber architecture and Hooks implementation of React.

More on the implementation and use of linked lists

Simulation setState

You can use linked lists to implement setState methods like React.

// Represents a node
class Update {
  constructor(payload, nextUpdate) {
    this.payload = payload
    this.nextUpdate = nextUpdate
  }
}
Copy the code

A node needs payload to mount data, and nextUpdate points to the next node.

// Simulate linked lists
class UpdateQueue {
  constructor() {
    this.baseState = null
    this.firstUpdate = null
    this.lastUpdate = null
  }
  enqueue(update) {
    if (!this.firstUpdate) {
      this.firstUpdate = this.lastUpdate = update
    } else {
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
}
Copy the code

FirstUpdate points to the first node and lastUpdate points to the last node.

And enQueue to chain the nodes.

const isFunction = (func) = > {
  return typeof func === 'function'
}
class UpdateQueue {
  forceUpdate() {
    let currentState = this.baseState || {}
    let currentUpdate = this.firstUpdate
    while(currentUpdate) {
      constnextState = isFunction(currentUpdate.payload) ? currentUpdate.payload(currentState) : currentUpdate.payload currentState = { ... currentState, ... nextState } currentUpdate = currentUpdate.nextUpdate }this.firstUpdate = this.lastUpdate = null
    return this.baseState = currentState
  }
}
Copy the code

ForceUpdate is also required to merge the data mounted on all nodes. Similar to the react.setstate () argument, objects and functions are available.

The Fiber architecture

What did you do before Fiber

Before Act15 and before React, React recursively compares the VirtualDOM tree to find nodes that need to be changed and then updates them synchronously. The process React is called Reconciliation.

During Reconciliation, React will be tied up in browser resources, either causing user triggered events to go unanswered, or causing frames to drop, causing users to feel stuck. The traversal process is simulated below.

The React15 DOMDIFF

Map the above node structure to a virtual DOM

const root = {
  key: 'A1'.children: [{key:  'B1'.children: [{key: 'C1'.children: []}, {key: 'C2'.children: []}]}, {key:  'B2'.children: []}]}Copy the code

The depth – first algorithm is used to traverse it

Rounding out the DFS

function walk(vdom, cb) {
  cb && cb(vdom)
  vdom.children.forEach(child= > walk(child, cb))
}
// Test
walk(root, (node) = > {
  console.log(node.key) // A1 B1 C1 C2 B2
})
Copy the code

The same is true with DOM-diff recursively traversing comparisons, and there are two problems that affect performance.

  • When the tree nodes are large, the recursive call execution stack gets deeper and deeper
  • Execution cannot be interrupted; the page waits for recursive execution to complete before being re-rendered

React dom-diff

What is the Fiber

  • Fiber is an execution unit
  • Fiber is also a data structure

Fiber is an execution unit

The browser task scheduling procedure above mentions that there is an idle phase, requestIdleCallback, after the page is composed.

The following figure shows the React scheduling process in the idle phase

This is cooperative scheduling that requires the application and browser to trust each other. The browser, as the leader, allocates a slice of execution time (requestIdleCallback) to the program to make the call, and the program needs to execute within this time by convention, returning control to the browser.

Fiber is an execution unit. After each execution unit, React checks how much time is left and returns control to the browser. Then proceed to render the next frame.

Fiber is also a data structure

React uses linked lists to link the Virtual DOM together, with each node representing a Fiber

class FiberNode {
  constructor(type, payload) {
    this.type = type // Node type
    this.key = payload.key // key
    this.payload = payload // Mount data
    this.return = null Fiber / / parent
    this.child = null / / the firstborn Fiber
    this.sibling = null // Adjacent brother Fiber}}// Test
const A1 = new FiberNode('div', { key: 'A1' })
const B1 = new FiberNode('div', { key: 'B1' })
const B2 = new FiberNode('div', { key: 'B2' })
const C1 = new FiberNode('div', { key: 'C1' })
const C2 = new FiberNode('div', { key: 'C2' })

A1.child = B1
B1.return = A1
B1.sibling = B2
B1.child = C1
B2.return = A1
C1.return = B1
C1.sibling = C2
C2.return =  B1
Copy the code

Fiber summary

  • Some scheduling policies can be used to properly allocate CPU resources to improve user response times
  • Making the Reconciliation process interruptible with Fiber allows the BROWSER to respond to user interactions in a timely manner by ceding execution to the CPU

Fiber Execution Phase

Each render has two phases: Reconciliation and Commit

  • Coordinate /render phase: This is the Diff phase, which can be interrupted. This phase finds out all node changes, such as additions, deletions, etc. These changes are called effects in React.
  • Commit phase: The side effects calculated in the previous phase that need to be addressed are executed once. This phase cannot be interrupted and must be executed simultaneously.

Reconciliation phase

The following will be mentioned above a few knowledge points in series to use.

This phase tests the example fiberrender.html, where the core code stores FiberRender.js.

Fiber is also a data structure summary above. We have built the Fiber tree and then started traversing. In the first rendering, all operation types are added.

Construct Fiber Tree according to Virtual DOM

nextUnitOfWork = A1
requestIdleCallback(workLoop, { timeout: 1000 })
Copy the code

Idle time to traverse and collect A1 root node

function workLoop (deadline) {
  / / this rendering and free time | | no timeout && still exist an execution unit
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // Executes the current execution unit and returns the next execution unit
  }
  if(! nextUnitOfWork) {console.log('render end ! ')}else {
    requestIdleCallback(workLoop, { timeout: 1000}}})Copy the code
  • When meetThis frame has idle time or no timeout && there is an execution unitTo execute the current execution unit and return the next execution unit.
  • If the above conditions are not met, if there is still an execution unit, the next frame will continue to render.
    • This phase completes when there is no execution unit.
function performUnitOfWork (fiber) {
  beginWork(fiber) / /
  if (fiber.child) {
    return fiber.child
  }
  while (fiber) {
    completeUnitOfWork(fiber) / / end
    if (fiber.sibling) {
      return fiber.sibling
    }
    fiber = fiber.return
  }
}
function beginWork (fiber) {
  console.log('start: ', fiber.key)
}
function completeUnitOfWork (fiber) {
  console.log('end: ', fiber.key)
}

Copy the code

The flow of traversing the execution unit is as follows

  1. Traversal starts at the root node
  2. If there is no firstborn, the current node traversal is complete.completeUnitOfWorkIn the collection
  3. If there are no adjacent brothers, the parent node is returned to indicate that the parent node traversal is complete.completeUnitOfWorkIn the collection
  4. If there is no parent node, all traversals are complete.over
  5. If there is a firstborn son, traversal;beginWorkIn the collection; After collecting, return to its eldest son, return toStep 2To iterate over
  6. If there are adjacent brothers, traversal;beginWorkIn the collection; After collecting, return to its eldest son, return toStep 2To iterate over

The collection sequence is as follows

Similar to sequential traversal of binary trees

function beginWork (fiber) {
  console.log('start: ', fiber.key) // A1 B1 C1 C2 B2
}
Copy the code

The collection is completed in the following order

A back-order traversal similar to a binary tree

function completeUnitOfWork (fiber) {
  console.log('end: ', fiber.key) // C1 C2 B1 B2 A1
}
Copy the code

The Commit phase

Similar to Git’s branch function, fork from the old tree, add, delete, update in the new branch, and then commit.

This phase tests the example fiberCommit.html, where the core code stores FiberCommit.js.

Construct the root fiber first, and the stateNode represents the real DOM of the current node.

let container = document.getElementById('root')
workInProgressRoot = {
  key: 'ROOT'.// Node instance (state) :
  // In the case of the host component, an instance of the host component, such as a DOM node, is saved
  // In the case of class components, the instance of the class component is saved here
  // For function components, this is empty because function components have no instances
  stateNode: container,
  props: { children: [A1] }
}
nextUnitOfWork = workInProgressRoot // Start with RootFiber and end with RootFiber
Copy the code

The beginWork collection process of the previous stage is improved. Fiber all nodes.

function beginWork(currentFiber) { // ++
  if(! currentFiber.stateNode) { currentFiber.stateNode =document.createElement(currentFiber.type) // Create the real DOM
    for (let key in currentFiber.props) { // Loop attributes assign values to the real DOM
      if(key ! = ='children'&& key ! = ='key')
        currentFiber.stateNode.setAttribute(key, currentFiber.props[key])
    }
  }
  let previousFiber
  currentFiber.props.children.forEach((child, index) = > {
    let childFiber = {
      tag: 'HOST'.type: child.type,
      key: child.key,
      props: child.props,
      return: currentFiber,
      // The side effect type of the current node, such as node update, deletion, move
      effectTag: 'PLACEMENT'.React also uses a linked list to connect all the fibers that have side effects
      nextEffect: null
    }
    if (index === 0) {
      currentFiber.child = childFiber
    } else {
      previousFiber.sibling = childFiber
    }
    previousFiber = childFiber
  })
}
Copy the code

Where effectTag identifies the side effect type of the current node, the first rendering is a PLACEMENT addition, and nextEffect identifies the next node with side effects.

Then perfect the completeUnitOfWork.

function completeUnitOfWork(currentFiber) { // ++
  const returnFiber = currentFiber.return
  if (returnFiber) {
    if(! returnFiber.firstEffect) { returnFiber.firstEffect = currentFiber.firstEffect }if (currentFiber.lastEffect) {
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
      }
      returnFiber.lastEffect = currentFiber.lastEffect
    }

    if (currentFiber.effectTag) {
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber
      } else {
        returnFiber.firstEffect = currentFiber
      }
      returnFiber.lastEffect = currentFiber
    }
  }
}
Copy the code

The goal is to form the completed collection into a linked list structure with the commitRoot phase.

After all the execution and completion is collected (i.e. all the real DOM, virtual DOM, and Fiber are combined to form a linked list of side effects), it needs to be rendered to the page.

function workLoop (deadline) {
  // ...
  if(! nextUnitOfWork) {console.log('render end ! ')
    commitRoot()
  } else {
    requestIdleCallback(workLoop, { timeout: 1000}}})Copy the code

Find the fiber node where the first side effect is completed and recursively appendChild to the parent element.

function commitRoot() { // ++
  let fiber = workInProgressRoot.firstEffect
  while (fiber) {
    console.log('complete: ', fiber.key) // C1 C2 B1 B2 A1
    commitWork(fiber)
    fiber = fiber.nextEffect
  }
  workInProgressRoot = null
}
function commitWork(currentFiber) {
  currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
}
Copy the code

Below is the collection sequence for the above rendered effects and prints

The React with Fiber

Prepare the environment

Create a project fiber using react-create-app

// src/index.js
import React from 'react'
let element = (
  <div id="A1">
    <div id="B1">
      <div id="C1"></div>
      <div id="C2"></div>
    </div>
    <div id="B2"></div>
  </div>
)
console.log(element);
Copy the code

NPM I && NPM start prints the following result

Borrowing the scaffolding’s Babel compilation, we write JSX syntax code directly.

Implement the createElement method

Convert the JSX syntax to an object at Babel compile time, and then build the virtual DOM by calling the react. CreateElement method under React. We can simulate it as follows:

// core/react.js
const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
function createElement(type, config, ... children) {
  return {
    type, // Element type
    props: {
      ...config,
      children: children.map(
        child= > typeof child === "object" ?
          child :
          { type: ELEMENT_TEXT, props: { text: child, children: []}})}}}let React = {
  createElement
}
export default React;
Copy the code

If a child in children is a React. CreateElement element that returns a React element and is a string, it will be converted to a text node.

Implementing the first render

Prepare the following structure

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
let style = { border: '3px solid green'.margin: '5px' };
let element = (
  <div id="A1" style={style}>
    A1
    <div id="B1" style={style}>
      B1
      <div id="C1" style={style}>C1</div>
      <div id="C2" style={style}>C2</div>
    </div>
    <div id="B2" style={style}>B2</div>
  </div>
)
ReactDOM.render(
  element,
  document.getElementById('root'));Copy the code

Desired render results

At this point, you need to define a list of constants

// core/constants.js
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT'); // Text element
export const TAG_ROOT = Symbol.for('TAG_ROOT'); / / root Fiber
export const TAG_HOST = Symbol.for('TAG_HOST'); // Native node span div p function component class component
export const TAG_TEXT = Symbol.for('TAG_TEXT'); // Text node
export const PLACEMENT = Symbol.for('PLACEMENT'); // Insert the node
Copy the code

Then, with the aforementioned Reconciliation stage, the virtual DOM is first constructed as a fiber tree in react-dom.js

// core/react-dom.js
import { TAG_ROOT } from './constants';
import { scheduleRoot } from './scheduler';
function render(element, container) {
  let rootFiber = {
    tag: TAG_ROOT, // This is a Fiber
    stateNode: container, // The DOM node corresponding to this Fiber
    props: { children: [element] }, // The child element is the element to render
  }
  scheduleRoot(rootFiber);
}

export default {
  render
}
Copy the code

It is then handed to the scheduleRoot for scheduling

// core/scheduler.js
// ...
Copy the code

There are a large number of codes, mainly for the Reconciliation and Commit phases.

The address where the procedure code is stored

The beginWork is refined

function beginWork(currentFiber) {
  if (currentFiber.tag === TAG_ROOT) { // If it is the root node
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_TEXT) { // If it is a native text node
    updateHostText(currentFiber);
  } else if (currentFiber.tag === TAG_HOST) { // If it is a native DOM nodeupdateHostComponent(currentFiber); }}function updateHostRoot(currentFiber) { // If it is the root node
  const newChildren = currentFiber.props.children; // Render the child node directly
  reconcileChildren(currentFiber, newChildren);
}
function updateHostText(currentFiber) {
  if(! currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber);// Create a real DOM node}}function updateHostComponent(currentFiber) { // If it is a native DOM node
  if(! currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber);// Create a real DOM node
  }
  const newChildren = currentFiber.props.children;
  reconcileChildren(currentFiber, newChildren);
}
Copy the code

StateNode is assigned to different types of nodes

  • Native DOM node/native text node: Directly create real DOM nodes assigned tostateNode
  • This will be extended below
    • Class component: Requires a new component instance to mount tostateNode
    • Functional components: No instances,stateNodenull

ReconcileChildren also deals with different types of nodes.

Render summary

Restate the two phases and scheduling rules from the previous section

  • There are two main phases to rendering and scheduling from the root node
    • Render phase: This phase takes time, we can split the task, split dimension virtual DOM. This stage is aided byrequestIdleCallbackYou can pause it
    • Diff phase: The new and old virtual DOM is compared to increment, update, and create
  • The render stage result iseffect listTo collect node additions, deletions, and changes
  • The Render phase has two tasks
    • Generate fiber tree from virtual DOM
    • collecteffectlist
  • Commit stage, the DOM update creation stage, this stage can not be paused, to complete

Scheduling rules

  • Traversal chain rule: first eldest son, then brother, then second uncle
  • Completion chain rule: if all sons finish, they finish by themselves
  • Effect chain: Cocompletion chain

Implement updates to elements

The double buffering optimization strategy is used, which will be highlighted below

Similar to the double buffering technique commonly used by graphical domain rendering engines. The image is first drawn to a buffer and then transmitted to the screen once for display, which prevents screen shake and optimizes rendering performance.

The operation page is then re-rendered, expecting the first update to change A1/B1/C1/C2 and add B3, and the second update to change A1/B1/C1/C2 and delete B3.

The new code is as follows

<! -- public/index.html -->
<div id="root"></div>
<button id="reRender1">reRender1</button>
<button id="reRender2">reRender2</button>
<button id="reRender3">reRender3</button>
Copy the code

Bind events for both buttons to re-render the page

// src/index.js
let reRender2 = document.getElementById('reRender2');
reRender2.addEventListener('click'.() = > {
  let element2 = (
    <div id="A1-new" style={style}>
      A1-new
      <div id="B1-new" style={style}>
        B1-new
        <div id="C1-new" style={style}>C1-new</div>
        <div id="C2-new" style={style}>C2-new</div>
      </div>
      <div id="B2" style={style}>B2</div>
      <div id="B3" style={style}>B3</div>
    </div>
  )
  ReactDOM.render(
    element2,
    document.getElementById('root')); });let reRender3 = document.getElementById('reRender3');
reRender3.addEventListener('click'.() = > {
  let element3 = (
    <div id="A1-new2" style={style}>
      A1-new2
      <div id="B1-new2" style={style}>
        B1-new2
        <div id="C1-new2" style={style}>C1-new2</div>
        <div id="C2-new2" style={style}>C2-new2</div>
      </div>
      <div id="B2" style={style}>B2</div>
    </div>
  )
  ReactDOM.render(
    element3,
    document.getElementById('root')); });Copy the code

Double buffered update policy

  • Assign the fiber tree after each rendering tocurrentRoot
  • The first update will berooterFiberthealternatePoint to theCurrentRoot was last rendered
  • Updates after the second one willworkInProgressRootPoint to thecurrentRoot.alternateAnd then put the currentworkInProgressRoot.alternatePoint to theCurrentRoot was last rendered
  • .
  • Then achieve the multiplexing fiber object tree
The change code is as follows
import { setProps } from './utils';
import {
    ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, PLACEMENT, DELETION, UPDATE
} from './constants';
+let currentRoot = null; // The current root Fiberlet workInProgressRoot = null; Fiber let nextUnitOfWork = null// nextUnitOfWork+let deletions = []; // The fiber node to be removed

export function scheduleRoot(rootFiber) {
  // {tag:TAG_ROOT,stateNode:container,props: { children: [element] }}
+ if (currentRoot && currentRoot. Alternate) {// An even number of updates
+ workInProgressRoot = currentRoot.alternate;
+ workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null;
+ workInProgressRoot.props = rootFiber.props;
+ workInProgressRoot.alternate = currentRoot;
+} else if (currentRoot) {// An odd number of updates
+ rootFiber.alternate = currentRoot;
+ workInProgressRoot = rootFiber;
+ } else {
+ workInProgressRoot = rootFiber; // First render
+}
    nextUnitOfWork = workInProgressRoot;
}

function commitRoot() {
+ deletions.forEach(commitWork);
  let currentFiber = workInProgressRoot.firstEffect;
  while (currentFiber) {
    commitWork(currentFiber);
    currentFiber = currentFiber.nextEffect;
  }
+ deletions.length = 0; // Delete the node to be deleted
+ currentRoot = workInProgressRoot;workInProgressRoot = null; } function commitWork(currentFiber) { if (! currentFiber) { return; } let returnFiber = currentFiber.return; Const domReturn = returnfiber.statenode; // Get the parent DOM node if (currentFiber. EffectTag=== PLACEMENT && currentFiber.stateNode ! = null) {// If it is a new DOM node
    let nextFiber = currentFiber;
    domReturn.appendChild(nextFiber.stateNode);
+} else if (currentFiber.effectTag === DELETION) {// Delete and return
+ domReturn.removeChild(currentFiber.stateNode);
+ } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode ! = null) {// If it is an update
+ if (currentFiber.type === ELEMENT_TEXT) {
+ if (currentFiber.alternate.props.text ! = currentFiber.props.text) {
+ currentFiber.stateNode.textContent = currentFiber.props.text;
+}
+ } else {
+ updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props);
+}
+}currentFiber.effectTag = null; } function reconcileChildren(currentFiber, newChildren) { let newChildIndex = 0; // The index in the new virtual DOM array+ let oldFiber = currentFiber.alternate && currentFiber.alternate.child; // The first child of parent Fiber is Fiber
+ let prevSibling;
+ while (newChildIndex < newChildren.length || oldFiber) {
+ const newChild = newChildren[newChildIndex];
+ let newFiber;
+ const sameType = oldFiber && newChild && newChild.type === oldFiber.type; // Both old and new, and the element type is the same
+ let tag;
+ if (newChild && newChild.type === ELEMENT_TEXT) {
+ tag = TAG_TEXT; / / text
+ } else if (newChild && typeof newChild.type === 'string') {
+ tag = TAG_HOST; // Native DOM component
+}
+ if (sameType) {
+ if (oldFiber.alternate) {
+ newFiber = oldFiber.alternate;
+ newFiber.props = newChild.props;
+ newFiber.alternate = oldFiber;
+ newFiber.effectTag = UPDATE;
+ newFiber.nextEffect = null;
+ } else {
+ newFiber = {
+ tag:oldFiber. Tag,// Marks Fiber type, such as function component or native component
+ type: oldfibre. type,// Specifies the element type
+ props: newchild. props,// New properties object
+ oldfibre. stateNode: oldfibre. stateNode: oldfibre. stateNode: oldfibre. stateNode: oldfibre. stateNode: oldfibre. stateNode: oldfibre. stateNode: oldFiber
+ return currentFiber,// Parent Fiber
+ alternate: oldFiber,// Last Fiber points to a node in the old tree
+ effectTag: UPDATE,// Side effect identifier
+ nextEffect: null //React also uses a linked list to connect all fibers with side effects
+}# +}+ } else {
+ if (newChild) {// Create a new Fiber
+ newFiber = {
+ tag,// native DOM component
+ type: newChild.type,// The specific element type
+ props: newchild. props,// New properties object
+ stateNode: null,//stateNode must be null
+ return currentFiber,// Parent Fiber
+ effectTag: PLACEMENT// Side effect identification
+}
+}
+ if (oldFiber) {
+ oldFiber.effectTag = DELETION;
+ deletions.push(oldFiber);
+}
+}
+ if (oldFiber) {// after comparing an element, the oldFiber moves back 1 bit
+ oldFiber = oldFiber.sibling;
+}
    if (newFiber) {
      if (newChildIndex = = = 0) {currentFiber.child = newFiber; // the first child is attached to the parent's child attribute} else {prevSibling. Sibling = newFiber; } prevSibling = newFiber; } prevSibling = newFiber; // newFiber becomes the previous sibling newChildIndex++; }}Copy the code

Implementation class component

Building a counter

class ClassCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { number: 0 };
  }
  onClick = () = > {
    this.setState(state= > ({ number: state.number + 1 }));
  }
  render() {
    return (
      <div id="counter">
        <span>{this.state.number}</span>
        <button onClick={this.onClick}>Add 1</button>
      </div >
    )
  }
}
ReactDOM.render(
  <ClassCounter />,
  document.getElementById('root')
);
Copy the code
import { ELEMENT_TEXT } from './constants';
+import { Update, UpdateQueue } from './updateQueue';
+import { scheduleRoot } from './scheduler';
// ...
+class Component {
+ constructor(props) {
+ this.props = props;
+ this.updateQueue = new UpdateQueue();
+}
+ setState(payload) {
+ this.internalFiber.updateQueue.enqueueUpdate(new Update(payload));
+ scheduleRoot();
+}
+}
+Component.prototype.isReactComponent = true;
let React = {
    createElement,
+ Component
}
export default React;
Copy the code

This process is illustrated in the simulation setState procedure

export class Update {
  constructor(payload) {
    this.payload = payload; }}// The data structure is a single linked list
export class UpdateQueue {
  constructor() {
    this.firstUpdate = null;
    this.lastUpdate = null;
  }
  enqueueUpdate(update) {
    if (this.lastUpdate === null) {
      this.firstUpdate = this.lastUpdate = update;
    } else {
      this.lastUpdate.nextUpdate = update;
      this.lastUpdate = update; }}forceUpdate(state) {
    let currentUpdate = this.firstUpdate;
    while (currentUpdate) {
      let nextState = typeof currentUpdate.payload === 'function'? currentUpdate.payload(state) : currentUpdate.payload; state = { ... state, ... nextState }; currentUpdate = currentUpdate.nextUpdate; }this.firstUpdate = this.lastUpdate = null;
    returnstate; }}Copy the code

The following changes need to be made in the SRC /scheduler.js file

function beginWork(currentFiber) {
  if (currentFiber.tag === TAG_ROOT) {// If it is the root node
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_TEXT) {// If it is a native text node
    updateHostText(currentFiber);
  } else if (currentFiber.tag // If it is a native DOM node
    updateHostComponent(currentFiber);
+} else if (currentFiber.tag === TAG_CLASS) {// If it is a class component
+ updateClassComponent(currentFiber)
+}
}
+function updateClassComponent(currentFiber) {
+ if (currentFiber.stateNode === null) {
+ currentFiber.stateNode = new currentFiber.type(currentFiber.props);
+ currentFiber.stateNode.internalFiber = currentFiber;
+ currentFiber.updateQueue = new UpdateQueue();
+}
+ currentFiber.stateNode.state = currentFiber.updateQueue.forceUpdate(currentFiber.stateNode.state);
+ const newChildren = [currentFiber.stateNode.render()];
+ reconcileChildren(currentFiber, newChildren);
+}
Copy the code

In the case of a class component, the new class cashes the instance to the CurrentFiber.statenode, and executes the result of the Render () method on the instance to recursively schedule the reconcileChildren

Implement functional components

Like similar components, add an else in each corresponding place.. If you can

function FunctionCounter() {
  return (
    <h1>
      Count:0
    </h1>
  )
}
ReactDOM.render(
  <FunctionCounter />.document.getElementById('root'));Copy the code
function beginWork(currentFiber) {
  if (currentFiber.tag === TAG_ROOT) {// If it is the root node
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_TEXT) {// If it is a native text node
    updateHostText(currentFiber);
  } else if (currentFiber.tag // If it is a native DOM node
    updateHostComponent(currentFiber);
  } else if (currentFiber.tag === TAG_CLASS) {// If it is a class component
    updateClassComponent(currentFiber)
+} else if (currentFiber. Tag === TAG_FUNCTION) {// If (currentFiber. Tag === TAG_FUNCTION)
+ updateFunctionComponent(currentFiber);
+}
}
+function updateFunctionComponent(currentFiber) {
+ const newChildren = [currentFiber.type(currentFiber.props)];
+ reconcileChildren(currentFiber, newChildren);
+}
Copy the code

Unlike a class component, a functional component has no instance and schedules the return value of function execution recursively.

Realize the Hooks

Use the following

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
// import React from '.. /.. /.. /packages/fiber/core/react';
// import ReactDOM from '.. /.. /.. /packages/fiber/core/react-dom';

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return { count: state.count + 1 };
    default:
      returnstate; }}function FunctionCounter() {
  const [numberState, setNumberState] = React.useState({ number: 0 });
  const [countState, dispatch] = React.useReducer(reducer, { count: 0 });
  return (
    <div>
      <h1 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}>
        Count: {numberState.number}
      </h1 >
      <hr />
      <h1 onClick={() => dispatch({ type: 'ADD' })}>
        Count: {countState.count}
      </h1 >
    </div>
  )
}
ReactDOM.render(
  <FunctionCounter />,
  document.getElementById('root')
);

Copy the code

React provides useState/useReducer hooks

// core/react.js
+import { scheduleRoot,useState,useReducer} from './scheduler';
let React = {
  createElement,
  Component,
+ useState,
+ useReducer
}
Copy the code

The implementation process is as follows

// core/scheduler.js
+import { UpdateQueue, Update } from './updateQueue';
+let workInProgressFiber = null; // Working fiber
+let hookIndex = 0; / / hook index
function updateFunctionComponent(currentFiber) {
+ workInProgressFiber = currentFiber;
+ hookIndex = 0;
+ workInProgressFiber.hooks = [];
  const newChildren = [currentFiber.type(currentFiber.props)];
  reconcileChildren(currentFiber, newChildren);
}
+export function useReducer(reducer, initialValue) {
+ let oldHook =
+ workInProgressFiber.alternate &&
+ workInProgressFiber.alternate.hooks &&
+ workInProgressFiber.alternate.hooks[hookIndex];
+ let newHook = oldHook;
+ if (oldHook) {
+ oldHook.state = oldHook.updateQueue.forceUpdate(oldHook.state);
+ } else {
+ newHook = {
+ state: initialValue,
+ updateQueue: new UpdateQueue()
+};
+}
+ const dispatch = action => {
+ newHook.updateQueue.enqueueUpdate(
+ new Update(reducer ? reducer(newHook.state, action) : action)
+);
+ scheduleRoot();
+}
+ workInProgressFiber.hooks[hookIndex++] = newHook;
+ return [newHook.state, dispatch];
+}
+export function useState(initState) {
+ return useReducer(null, initState)
+}
Copy the code

conclusion

After looking at the very dry and simple implementation above, let’s review the first few questions:

  • What are the pain points of Act15? What is Fiber? Why did Act16 need Fiber?
    • The render and diff phases happen at the same time, and the page freezes when the node tree is large
    • Fiber is no mystery, it just turns Virtual-DOM into a linked list structure
    • The linked list structure and requestIdleCallback can realize the interruptible and recoverable scheduling mechanism
  • How to implement the virtual DOM under Act16?
    • With React15
  • How to implement Fiber data structure and traversal calculation method?
    • See Fiber for another data structure
  • How to realize interruptible and recoverable task scheduling in Fiber architecture?
    • How do I specify quantity updates? How to batch update?
    • With the help of requestIdleCallback, which forces the browser to implement a specified amount of update within the given idle time after a frame is rendered, batch updates can skip this API and continue as before
  • How to implement component rendering and side effect collection submission in Fiber architecture?
    • The collection order performed is similar to the sequential traversal of a binary tree
    • The collection order completed is similar to a back-order traversal of a binary tree
  • How to implement harmonic and double buffering optimization strategies in Fiber?
    • Add an alternate field to the Fiber structure to identify the Fiber tree that was rendered last time and can be reused next time
  • How to implement Hooks such as useReducer and useState?
  • How to implement the expirationTime task priority task scheduling timeout processing?
  • How to optimize key processing for Reconcile Domdiff?
  • How to implement a SyntheticEvent?
  • How to implement ref useEffect?

But there are still a few questions left unanswered, which will be explored in the next article…

The resources

  • facebook/react
  • React Fiber Architecture – James Seto
  • This is probably the most popular way to open React Fiber