Hit the React-Mini version of the code

Recently, I learned an article about the basic implementation of React principle. I have learned a lot and would like to write my personal gains and summary here.

0, JSX

const element = (
    <input value="todo" />
)
Copy the code

Write the simplest JSX that we know will be converted to an Object, as shown below

const element = {
    type: 'input'.props: {
        value: 'todo'.children: [].}}Copy the code

1, the createElement method

How do you convert JSX into an Object? So I need to write a function called createElement to do that

function createElement(type, props, ... children) {
  return {
    type,
    props: {
      ...props,
      children
    }
  }
}

const element = createElement(
  'input',
  {
    value: 'todo'})Copy the code

But what about JSX with nested child elements, for example

const element = (
    <div id="foo">
      <b />
      <b />
    </div>
)
Copy the code

So it’s going to be the following

/* @return { type: 'div', props: { id: 'foo', children: [ { type: 'b', props: { children: [] } }, { type: 'b', props: { children: [] } }, ] } } * / const element = createElement( 'div', { id: 'foo' }, createElement('b'), createElement('b'), )Copy the code

There is, of course, a special case where the child element contains text

const element = (
  <div id="foo">
    <b />
    bar
  </div>
)
Copy the code

So call time is zero

/* @return { type: 'div', props: { id: 'foo', children: [ { type: 'b', props: { children: [] } }, 'bar' ] } } */
const element = createElement(
  'div',
  {
    id: 'foo'
  },
  createElement('b'),
  'bar'
)
Copy the code

As you can see, the children array in div contains an object and a string ‘bar’. For further processing, we need to convert the text element to an object we agreed on, as follows: createElement

function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT'.props: {
      nodeValue: text,
      children: [],}}}function createElement(type, props, ... children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child= >
        typeof child === 'object'
          ? child
          : createTextElement(child)
      )
    }
  }
}
Copy the code

Eventually convert to

{
  "type": "div"."props": {
    "id": "foo"."children": [{"type": "b"."props": { "children": []}}, {"type": "TEXT_ELEMENT"."props": { "nodeValue": "bar"."children": []}}]}}Copy the code

2, render

Now that we have the JSX object, we need to call reactdom. render to render the page container

const container = document.getElementById('root')
ReactDOM.render(element, container)
Copy the code

Render method basic implementation, as follows

function render(element, container) {
  const dom =
    element.type == 'TEXT_ELEMENT'
      ? document.createTextNode(' ')
      : document.createElement(element.type)

  // All properties on element.props, except children, are dom node properties
  const isProperty = key= >key ! = ='children'
  
  // Add attributes to the DOM
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name= > {
      dom[name] = element.props[name]
    })

  // Recursively iterate over the child elements in the DOM
  element.props.children.forEach(child= >
    render(child, dom)
  )

  container.appendChild(dom)
}

const ReactDOM = {
  render
}
Copy the code

3, concurrent mode

As of the above, we can write the JSX and give it to createElemnt to convert to a JS object and finally to Render.

Render uses recursive traversal above, which can be a bit problematic. Once render is executed, the render function does not end until the DOM tree is rendered. If the DOM tree is very large, it might block the main thread for too long. If the browser needs to handle high-priority work such as user input or smooth animations, it must wait until the rendering is complete.

React uses idle time (which does not affect the delay of critical events such as animations and input responses) to build the virtual DOM tree. Finally, the entire virtual DOM tree is built before rendering. How does React allow free time for virtual DOM builds?

React has deprecated the requestIdleCallback method, however, due to some issues with the API (not executing frequently enough for smooth UI rendering, compatibility, etc.).

Github.com/facebook/re…

Github.com/hushicai/hu…

However, we can still use this API to implement the Concurrent mode functionality of the simple React version. Below, we call requestIdleCallback over and over again, executing tasks as soon as we find that there is enough free time in each frame and that there are still unfinished tasks.

let nextUnitOfWork = null

function workLoop(deadline) {
  // Whether to stop
  let shouldYield = false

  while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork)// Stop if the idle time is less than 1ms
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)

function performUnitOfWork() {
  // ...
}
Copy the code

4. Fiber model

Let’s move on to the Fiber model. Let’s say you have a piece of JSX

const element = (
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>
)
Copy the code

The createElement method used previously would have converted each element in JSX to an object with attributes like Type, children, and so on. Based on this, we are going to enrich these objects and design them as fiber models. Rules are

  • Every element has an attribute that points to its parent (except the root element)
  • Each element has an attribute that points to its first child (if it has a son)
  • Each element has an attribute that points to its adjacent sibling element (if any)

The Render method in Section 2 is a one-time recursive render from the root node. Now we must optimize the, add each element to add attributes and child elements of the operations are divided into individual tasks, use idle time to perform a task, when there is no free time, record the current task and stop execution, continue to the next free time, until to complete all the tasks, then rendering task can be achieved. Also, there’s a key function performUnitOfWork that needs to be implemented in the last section

function performUnitOfWork(fiber) {
  if(! fiber.dom) {// Create the dom fragment corresponding to fiber
    fiber.dom = createDom(fiber)
  }

  const elements = fiber.props.children
  let index = 0
  let prevSibling = null

  while (index < elements.length) {
    const element = elements[index]
    const newFiber = {
      type: element.type,
      props: element.props,
      // point to the parent element
      parent: fiber,
      dom: null,}// The parent element only needs to refer to its first child
    if (index === 0) {
      fiber.child = newFiber
    } else {
      // Each element needs to refer to its next sibling
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }

  if (fiber.child) {
    return fiber.child
  }

  let nextFiber = fiber
  while (nextFiber) {
    // If there is a next sibling element, otherwise the parent element's next sibling element will be looked up
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }

    nextFiber = nextFiber.parent
  }
}

function createDom(fiber) {
  const dom = 
    fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode(' ')
      : document.createElement(fiber.type)
    
  const isProperty = key= >key ! = ='children'
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name= > {
      dom[name] = fiber.props[name]
    })

  return dom
}
Copy the code

So the render function is changed to

let nextUnitOfWork = null
function workLoop(deadline) {
  // Whether to stop
  let shouldYield = false
  while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() <1
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)

function render(element, container) {
  // start render, nextUnitOfWork from the root node
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    }
  }
}

const ReactDOM = {
  createElement,
  render
}
Copy the code

5, Render and commit phases

The last section talked about using free time to execute each unit task in order to create the Fiber model, so when the entire Fiber virtual tree is built, it’s time to actually render the DOM.

So the question is, how do we know when the virtual tree is built and when we’re ready to render it?

Starting with a review of the Fiber model and the performUnitOfWork function, you can see that the process of building a virtual tree is similar to DFS (depth-first traversal). It traverses from the root to the bottom and back again until it returns to the root. Since the root node has no younger and parent elements, the build is complete (performUnitOfWork() returns undefined).

At this point we’ll change the render function again and declare a variable wipRoot to store the root node of the virtual tree under construction.

let nextUnitOfWork = null
// Record the root node
let wipRoot = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}
Copy the code

In concurrent mode 3, we learned that React-Mini would call requestIdleCallback repeatedly to implement the sharding task, so this is where we determine whether the tree is complete and decide to start rendering.

function workLoop(deadline) {
  let shouldYield = false
  while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
  }

  if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop)Copy the code

The commitRoot function is simple

function commitRoot() {
  commitWork(wipRoot.child)
  // Reset the root node
  wipRoot = null
}

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

  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

6, Reconciliation

At this point, we’ve done our initial rendering work. What about updating the render again? We need to consider updating the DOM and deleting the DOM.

First of all, we don’t need to completely build a new DOM tree to render again, we can compare the current virtual tree with the virtual tree to render, and only need to change or delete elements to minimize unnecessary rendering.

Therefore, we need a variable to record the current virtual tree and establish a relationship with the virtual tree to be rendered

let nextUnitOfWork = null
// Current virtual root node
let currentRoot = null
// Root node of the virtual tree in progress
let wipRoot = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    // Build relationships
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}

function commitRoot() {
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
Copy the code

So the comparison process is in formUnitofwork, and we need to change that

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

  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
Copy the code

There is a function for reconcileChildren, whose main function is fiber contrast.

// oldFiber: div --> div --> div --> div
// elements: div --> div --> p
Update update add delete
Copy the code

Here is a comparison of the old Fiber and the new JSX in position order.

  • Update an element of the same type in the same position.
  • If the element type is different but the location is the same, add is added.
  • If the old fiber has elements in the same position but the new JSX has no elements, delete is deleted.

Take the example above. React does not compare keys, but compares them in order of position. React adds keys to elements. React.docschina.org/docs/lists-…

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

    // Check whether the old element and the new element are of the same type
    const sameType = oldFiber && element && element.type == oldFiber.type

    if (sameType) {
      // update
    }
    if(element && ! sameType) {// add
    }
    if(oldFiber && ! sameType) {// delete
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}
Copy the code

Then how to set the new newFiber specifically? We added an extra field effectTag to indicate which newFiber was added, deleted or modified, which was convenient for judging in the subsequent rendering task.

if (sameType) {
  // update
  newFiber = {
    type: oldFiber.type,
    props: element.props,
    dom: oldFiber.dom,
    parent: wipFiber,
    alternate: oldFiber,
    effectTag: 'UPDATE',}}if(element && ! sameType) {// add
  newFiber = {
    type: element.type,
    props: element.props,
    dom: null.parent: wipFiber,
    alternate: null.effectTag: 'PLACEMENT',}}if(oldFiber && ! sameType) {// delete
  oldFiber.effectTag = 'DELETION'
  deletions.push(oldFiber)
}
Copy the code

Since the old fibers that need to be deleted do not need to be put back into the virtual tree, they are stored separately with deletions array variables, and the corresponding DOM is removed through the number group during subsequent rendering.

Deletions also need to go into other functions.

let nextUnitOfWork = null
// Current virtual root node
let currentRoot = null
// Root node of the virtual tree in progress
let wipRoot = null
// The node to be deleted
let deletions = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  // Reset the node to be deleted
  deletions = []
  nextUnitOfWork = wipRoot
}
function commitRoot() {
  // Iterate over the unmount node
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
Copy the code

Now that fiber comparison is over, it’s time for rendering. We need to modify the commitWork function to add, delete, and modify fibers with different effectTags.

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) {
    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

A new function, updateDom, is used to add, delete, or modify attributes on the DOM.

const isEvent = key= > key.startsWith('on')
const isProperty = key= >key ! = ='children' && !isEvent(key)
const isNew = (prev, next) = > key= >prev[key] ! == next[key]const isGone = (prev, next) = > key= >! (keyin next)
function updateDom(dom, prevProps, nextProps) {
  // Remove old or changed event listeners
  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]
      )
    })

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

  // Sets new or changed properties
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name= > {
      dom[name] = nextProps[name]
    })

  // Add event listener
  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

Since the createDom function did not allow for event listening, change it

function createDom(fiber) {
  const dom =
    fiber.type == 'TEXT_ELEMENT'
      ? document.createTextNode(' ')
      : document.createElement(fiber.type)

  updateDom(dom, {}, fiber.props)

  return dom
}
Copy the code

The React-Mini’s main features are now complete. The latter is a supplement.

7, the Function of Components

Let’s go back to the createElement section

// write JSX
const element = (
  <input value="todo" />
)

// convert JSX
const element = createElement(
  'input',
  {
    value: 'todo'})// 3. Convert JSX to object
const element = {
  type: 'input'.props: {
    value: 'todo'.children: [].}}Copy the code

What if you write JSX as a function component

function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
Copy the code

According to the previous rules, it will also be converted

function App(props) {
  return Didact.createElement(
    'h1'.null.'Hi ',
    props.name
  )
}
const element = Didact.createElement(App, {
  name: 'foo',})const element = {
  type: App,
  props: {
    name: 'foo'.children: [{type: 'h1'.props: {
          children: [{type: 'TEXT_ELEMENT'.props: { 'nodeValue': 'Hi '.'children': []}}, {type: 'TEXT_ELEMENT'.props: { 'nodeValue': 'foo'.'children': []}}]}}Copy the code

What’s special here is that element’s attribute type is no longer a string of tag types, but a function. So the previous code needs to be changed again.

The first is the formUnitofwork function

function performUnitOfWork(fiber) {
  // if (! fiber.dom) {
  // fiber.dom = createDom(fiber)
  // }
  // const elements = fiber.props.children
  // reconcileChildren(fiber, elements)
  
  const isFunctionComponent = fiber.type instanceof Function

  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
  }

}
Copy the code

This is handled differently depending on the type attribute

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function updateHostComponent(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
  reconcileChildren(fiber, elements)
}
Copy the code

Then think about the features of function components, such as the following

function App() {
  return (
    <span>foo</span>)}const element = (
  <div id="root">
    <App />
  </div>
)

/ / fiber model
div --> App --> span

// Final render
<div>
  <span>foo</span>
</div>
Copy the code

In fact, there is no DOM corresponding to the Fiber node on the App layer, so the SPAN tag should cross the App node and be rendered as a child of the DIV tag. So review commitWork

function commitWork(fiber) {
  if(! fiber) {return
  }
  // We can't find parent-dom directly, but we can find parent-.parent-dom, or the next level
  // const domParent = fiber.parent.dom
  
  // Find the parent node of the DOM
  let domParentFiber = fiber.parent
  while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom
  
  // ...
}
Copy the code

The complete commitWork is below

function commitWork(fiber) {
  if(! fiber) {return
  }
  let domParentFiber = fiber.parent
  while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom
  
  if (fiber.effectTag === 'PLACEMENT'&& fiber.dom ! =null) {
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === 'UPDATE'&& fiber.dom ! =null) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === 'DELETION') {
    commitDeletion(fiber, domParent)
  }

  if(fiber.effectTag ! = ='DELETION') {
    commitWork(fiber.child)
    commitWork(fiber.sibling)
  }
}  
Copy the code

The same is true when deleting nodes

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}
Copy the code

8 Hooks.

Since hooks came out, there have been joys and sorrows. Happy people find it easy to use hooks, as in the simple example below

function Counter() {
  const [state, setState] = useState(1)
  return (
    <button onClick={()= > setState(c => c + 1)}>
      Count: {state}
    </button>)}Copy the code

But how exactly do hooks work? We need to start where we created the function component, which was the updateFunctionComponent function

// Fiber under construction
let wipFiber = null
// Record the hook execution position
let hookIndex = null

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
Copy the code

As you can see, we declared two variables, wipFiber and hookIndex, to store the fiber under construction and record the hook position, respectively.

Here is the implementation of useState

function useState(inital) {
  const oldHook = 
    wipFiber.alternate && 
    wipFiber.alternate.hooks && 
    wipFiber.alternate.hooks[hookIndex]
  
  const hook = {
    state: oldHook ? oldHook.state : inital,
    queue: [],}// Fiber stores hook state on the node
  wipFiber.hooks.push(hook)

  // Hook position forward
  hookIndex++
  return [hook.state]
}
Copy the code

Here a hook uses an array to store state, and each time a hook is used, the array moves forward. Therefore, the position of the old hook and the new hook should be placed one by one, so that the new hook can accurately depend on the state of the old hook. Thus is easy to understand why the React to the rules of using hook react.docschina.org/docs/hooks-…

React actually uses linked lists to store hook states, but arrays are used for convenience.

Currently only state is returned using useState, and setState needs to be added

function useState(inital) {
  const oldHook = 
    wipFiber.alternate && 
    wipFiber.alternate.hooks && 
    wipFiber.alternate.hooks[hookIndex]
  
  const hook = {
    state: oldHook ? oldHook.state : inital,
    queue: [],}const actions = oldHook ? oldHook.queue : []
  actions.forEach(action= > {
    const isFunction = action instanceof Function
    / / update the state
    hook.state = isFunction ? action(hook.state) : action
  })

  const setState = action= > {
    hook.queue.push(action)
    // Trigger virtual tree building and rendering like the render function
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }

  // Fiber stores hook state on the node
  wipFiber.hooks.push(hook)

  // Hook position forward
  hookIndex++
  return [hook.state, setState]
}
Copy the code

React Mini is now ready!

Full code github.com/Zeng-J/reac…

conclusion

After learning React, I got to know some basic concepts. However, React min has a lot of details to improve, such as not considering key comparison, not considering recycling old fiber, etc. Anyway, with the understanding of the basic concept, the follow-up liver source code is a little less laborious.

The article study

Pomb. Us/build – your -…