Written on the front

I’m learning the react source code, but I don’t have a clue, and I don’t know where to start. Reading the source code line by line is not an option. React build your own React function. Create a build-your-own-react function. Create a build-your-own-react function. Then, I guided you step by step and explained obscure concepts such as Fiber and concurrent mode in detail, and finally realized react. This article is a summary after I read it. As a knowledge carrier, I will share what I have digested with you in a more concise and understandable way

How JSX is parsed (createElement)

Here is a simple JSX syntax

const element = <h1 title="foo">Hello</h1>

Copy the code

It is parsed by Babel into the following code


const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)
​

Copy the code

Let’s implement a simple createElement that generates the virtual DOM


function createElement(type, props, ... children) {
  return {
    // Mark the element type
    type,
    // Attribute of the element
    props: {
      ...props,
      / / child elements
      children: children.map(child= >
        // To distinguish the primitive type from the reference type, create the text node with createTextElement alone
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}
​
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT".props: {
      nodeValue: text,
      children: [],},}}Copy the code

Step 2 We need to render to the actual DOM node

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
​
  // exclude special attribute "children"
  const isProperty = key= >key ! = ="children"

  // Write the element attributes to the DOM node
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name= > {
      dom[name] = element.props[name]
    })

​  // Traversal recursion attaches child elements one by one to the actual DOM node
  element.props.children.forEach(child= >
    render(child, dom)
  )
​
  // Finally mount to the specified DOM node container
  container.appendChild(dom)
}

Copy the code

Concurrent Mode

So far, we seem to have implemented another simplified version of React that can render JSX to the DOM, but there is one problem

That is, our render function uses recursion to implement patch to DOM. If our node hierarchy is large and there are many nodes, it may occupy the browser process for a long time, causing blocking and affecting the higher-priority transaction processing of the browser (for example, user input and UI interaction).

Since we need to break up this large task into smaller units of work, so that if the browser has a higher priority transaction, we can interrupt the render of react elements, we introduce a concept called “concurrent mode.”


let nextUnitOfWork = nullfunction workLoop(deadline) {
 // Whether to pause
  let shouldYield = false
  while(nextUnitOfWork && ! shouldYield) {// Executes one unit of work and returns the next
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    // Determine whether free time is sufficient
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}
Copy the code

Step 4 What is Fiber and why do we need it

What is Fiber?

The structure of the Fiber

Above is a Fiber tree. To organize units of work, we need a data structure, each element has a filber structure, and each fiber is a unit of work

In how each unit of work works, the following function performUnitOfWork does three main things:

  1. Add elements to the DOM
  2. Create a fiber structure for each of the element’s children
  3. Find the next unit of work


function performUnitOfWork(fiber) {
  // Create a DOM element and attach it to the FIBER dom attribute
  if(! fiber.dom) { fiber.dom = createDom(fiber) }// Add dom to the parent element
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
​
  const elements = fiber.props.children
  let index = 0
  // Save the previous sibling fiber structure
  let prevSibling = nullwhile (index < elements.length) {
    const element = elements[index]
​
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,}// The first child is called child and the remaining children are called sibling
    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​    
    prevSibling = newFiber
    index++
  }
​  // Return child if there is child fiber
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber

  while (nextFiber) {
    // if sibling fiber exists, return SIBLING fiber
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    // step3, otherwise return his parent fiber
    nextFiber = nextFiber.parent
  }
}
​


Copy the code

What do you do in the Render and Commit phases of Step 5

In performUnitOfWork above, we’re adding elements directly to the DOM each time, and one of the problems is that the browser can interrupt us at any time, and we’re presenting the user with an incomplete UI, so we need to make some changes, After all the units of work are done, we add all the DOM together


function commitRoot() {
  // TODO add nodes to dom
}


function workLoop(deadline) {
  let shouldYield = false
  while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
  }
​  // After all units of work have been executed, we commit all elements to the DOM tree on the commitRoot
  if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) }Copy the code

Step 6 Reconciliation

So far, we have only dealt with adding the DOM. How about updating and removing the DOM? In this case, after the commit phase is complete, Use a variable to hold the old Fiber tree (called currentRoot) and the current (WipRoot: We also add an alternate property on each wipRoot to link the old Fiber tree (after the last commit).


function commitRoot() {
  commitWork(wipRoot.child)
  // Save the current Fiber tree after the commit phase is complete
  currentRoot = wipRoot
  wipRoot = null
}
​

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    // Establish a connection with the old Fiber tree from the last COMMIT phase
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}
​
let currentRoot = null

Copy the code

Here are the rules for comparison:

  1. If the old Fiber element and the new element have the same type, compare their attributes further
  2. If the type is different and there is a new element, you need to create a new DOM node
  3. If the type is different and there is an old fiber element, remove the old node

React also uses a key for comparison. For example, it detects that the position of a child element in an array of elements has changed.


function performUnitOfWork(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
  reconcileChildren(fiber, elements)
​
  // ignore
}

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while( index < elements.length || oldFiber ! =null
  ) {
    const element = elements[index]
    let newFiber = null

    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type
    // Same type, update attribute
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",}}// If the type is different, but a new fiber element exists, add it (add a new fiber element)
    if(element && ! sameType) { newFiber = {type: element.type,
        props: element.props,
        dom: null.parent: wipFiber,
        alternate: null.effectTag: "PLACEMENT",}}// If the old fiber tree exists, remove it (collect it first and remove it at commit)
    if(oldFiber && ! sameType) { oldFiber.effectTag ="DELETION"
      deletions.push(oldFiber)
    }
    // next loop to compare brother fiber (same as i++ below)
    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
    // If it is the first child, attach the new fiber to wipFiber's child property
    if (index === 0) {
      wipFiber.child = newFiber
    } else if (element) {
      // Attach other child elements to the sibling attribute of the previous child
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}


Copy the code

With the Reconcile phase completed, we enter the COMMIT phase

function commitRoot() {
  // Remove the old nodes that you just collected
  deletions.forEach(commitWork)
  // commit the child element of the current wipRoot
  commitWork(wipRoot.child)
  // Change the current root reference
  currentRoot = wipRoot
  wipRoot = null
}

function commitWork(fiber) {
  if(! fiber) {return
  }

  const domParent = fiber.parent.dom
  if (
    fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
  ) {
    domParent.appendChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE"&& fiber.dom ! =null
  ) {
   // Update dom attributes (add new attributes and remove old attributes) and add and remove events
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

The next step is to update the DOM

// Event attributes
const isEvent = key= > key.startsWith("on")
// Attributes except the event attribute and the special attribute children
const isProperty = key= >key ! = ="children" && !isEvent(key)
// Whether it is a new attribute
const isNew = (prev, next) = > key= >prev[key] ! == next[key]// Whether to remove the attribute
const isGone = (prev, next) = > key= >! (keyin next)

function updateDom(dom, prevProps, nextProps) {
  // Remove the old event
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key= >! (keyin nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name= > {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener(
        eventType,
        prevProps[name]
      )
    })

  // Remove the old attributes
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name= > {
      dom[name] = ""
    })

  // Add or update new attributes
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name= > {
      dom[name] = nextProps[name]
    })

  // Add listening events
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name= > {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
}

Copy the code

Step 7 Function Components

Next, let’s implement functional components


function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)

Copy the code

Functional components differ in two ways:

  1. Functional components without DOM nodes?
  2. His children property is not on props, but his return value

So we need to make the following changes


function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  // Distinguish functional components
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
​
function updateFunctionComponent(fiber) {
  // Execute the functional component to get children
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
​
function updateHostComponent(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) } reconcileChildren(fiber, fiber.props.children) }Copy the code

Then commitWork also needs to change:

function commitWork(fiber) {
  if(! fiber) {return
  }
​
  let domParentFiber = fiber.parent
 // Recursively find the element containing the DOM node
  while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom
​
  if (
    fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
  ) {
    domParent.appendChild(fiber.dom)
  } 
  // ignore

  else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent)
  }

  // ignore
 
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    // Remove the node until there are elements of the DOM node
    commitDeletion(fiber.child, domParent)
  }
}

Copy the code

Step 8 Hooks

As a final step, let’s add state to functional components

// Save the current fiber
let wipFiber = null
// Save the index of the currently executed hook, which hook is executed each time
let hookIndex = nullfunction updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
Copy the code

Next, implement the useState hook


function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    // If there is an old value, take it directly; otherwise, take the initial value passed in
    state: oldHook ? oldHook.state : initial,
    // Store the queue for each update
    queue: []};const actions = oldHook ? oldHook.queue : [];
  actions.forEach(action= > {
    hook.state = action(hook.state);
  });

  const setState = action= > {
    hook.queue.push(action);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    // Set to the next unit of work so that a new render can be started
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

Copy the code

summary

Here we have implemented a simple version of React. Although it differs from the actual React code, it is easier to understand the source code. If you want to study and read the source code further, React.iamkasong.com/

Original text: pomb. Us/build – your -… Translation: github.com/Yangfan2016…

IO /s/didact-8-…