6thfdwp / learning-thoughts

Record and Reflect
2 stars 0 forks source link

React Reconciliation Comparison - Simplified #5

Open 6thfdwp opened 3 years ago

6thfdwp commented 3 years ago

This learning starts from inspiring series of blog posts Didact: DIY your own React, which has been updated in his new blog post with Fiber and Hooks implementations (drastically simplified but core concept remains true).

Core Concepts Overview

The React contains 3 main packages

React Core

APIs necessary to define components. like React.createElement() and to define and update states

Reconcilers
This manages to generate next snapshot of UI (represented by element object tree) based on latest state, also be able to do diff and figure out the minimal updates (platform calls) the Renderer needs to take. It is more concerned with 'WHAT to render on screen'

Renderers

Renders manage how a React tree turns into underlying platform calls For example ReactDOM turns it to imperative, mutative calls to DOM API (appendChild, createTextNode..), ReactNative turns it into a single JSON message that lists mutations [['createView', attrs], ['manageChildren',] ...].

With this kind of separation, It allows different renderers to handle platform specific while reusing the same React core and reconciling algorithms. Renderers is mainly to encapsulate the 'HOW' part.

My learning and experimental code repo are focused on Reconciler algorithm and a bit of React core, to able to return the element object from the JSX

React Element

React.Element is light weight object representation of actual UI (e.g DOM in web)
Component is the definition to return the Element. It can compose other components using HTML like syntax (JSX) to create complext UI structure.

Let's say we have a list of stories (or any type of items), we can 'Like' each story and the number goes up. The component might look like this:

const StoryLike = ({ likes, url, name, onLike }) => (
  <li className='row'>
    <button onClick={onLike}>{likes} ❤️</button>
    <a href={url}>{name}</a>
  </li>
);

Before actual running, Babel plugin will recursively check the JSX and transpile each node to createElement call. For , it will be like:

createElement(
  // type
  'li',
  // props, will be null if no props
  { className: 'row' },
  // children as the rest of parameters
  // button is the element which only contains text elements (leaf)
  createElement('button', { onClick: onToggleLike }, likes, '\u2764\uFE0F'),
  createElement('a', { href: url }, name)
);

The returned element object tree representing <StoryLike> would be:

{
  type:'li',
  props:{
    className:'row',
    children: [
      {
        type:'button',
        props:{
          onClick: handler,
          children:[
            {type:'TEXT_ELEMENT', props:{nodeValue: ${likes}, children:[]}}
            {type:'TEXT_ELEMENT', props:{nodeValue: 'likes', children:[]}}
          ]
        }
      },
      {
        type:'a',
        props:{
          href:${url},
          children:[type:'TEXT_ELEMENT',...]
        }
      }
    ]}
}

So the first thing we need is to implement the simplified version of React.createElement, the function signature would be:

/**
 *  @param {string|function} type:
 *        either dom el 'div', 'span' etc. or custom component
 *  @param {object} config: properties speficied in JSX for each node
 *        like style, onClick..
 *  @param {?array-like} args: children of current element
 * /
const createElement = (type, props, ...rest) => {}

Stack Reconciler

This is the reconciling algorithm before React 16. This reconciler uses recursion to walk through the element object tree to build the internal instances hierarchy. As recursion cannot be interrupted once it's started, it could block browser UI thread and user interaction suffers when it takes long time, which is common for complex UI (e.g to render long list with complex data)

Consider we have an App which renders only one StoryLike component

const story = {
    name: 'Didact introduction',
    url: 'http://bit.ly/2pX7HNn',
    likes: 12,
}
// App.js
render() {
  <div >
    <h1>{props.title}</h1>
    <StoryLike story={story} />
  </div>
}

const StoryLike = ({story, onClick}) => {
  return (
      <li id="story-item">
        <button
          onClick={(e) => onClick()}
        >
          {story.likes} ❤️
        </button>
        <a id="story-link" href={story.url}>
          {story.name}
        </a>
      </li>
  )
}

When render(<App title='Stack Reconciler' />), it recursively builds the internal instance hierarchy corresponding to each level in the elemement object tree. There are two main types of instances for two types of element, one for primitive whose type is string (e.g div, li), one for custom component which has type 'function'

The internal instance hierarchy can be represented as below:

CompositeComponent App
 > currentElement: {type: App(function), props:{title, children:[]}}
 > publicInstance: new App()
 > renderedComponent: DOMComponent
   > currentElement: {type:"div", props:{children:[..]}}
   > node: div --> 1.
   > renderedChildren: [
     DOMComponent: {
       > currentElement: {type:'h1'},
       > node: h1 --> 1.1
       > renderedChildren: [DOMComponent]
     CompositeComponent: StoryLike
       > currentElement: {type: StoryLike, props:{children:[]}}
       > publicInstance: new StoryLike()
       > renderedComponent: DOMComponent {
         > currentElement {type:'li', ..}
         > node: li --> 1.2.1
         > renderedChildren: [
           DOMComponent button, --> 1.2.1.1
           DOMComponent a  --> 1.2.1.2
         ]
   ]

Fiber reconciler

We could also call it incremental reconciler. It still needs to traverse the element object tree, just the process can be split into chunks and spread it out over multiple call stack frames. Compared to Stack Reconciler, it does not rely on recursion that has to be finished in one single call stack, avoid blocking UI thread.

In this implementation, it demonstrates a simple scheduling to split traversal via requestIdleCallback. It's more like building a linked list incrementally. The element object tree is transformed to fiber nodes linked together in parent → first child → sibling and back to parent fashion.

The same render(<App title='Fiber Reconciler' />) above, its reconciliation process can be visualised as below: image

From 1 to 17, it can be interrupted at any time based on the priorities or time is up for browser to draw current frame in the UI thread every 16.6ms (60fps frames per second, means 1000ms/60 = 16.6ms per frame)

The final commit phase is to actually do DOM operation (place new nodes, update or deletion), which need to be done in one go, so user can see full content / style painted at once.

6thfdwp commented 3 years ago

Refs

Fiber Principles from React Core team

Build your own React: simplified
Under the hood: ReactJS
Deep Dive in React execution https://react.iamkasong.com/preparation/idea.html

Video
Build a Custom React Renderer

6thfdwp commented 3 years ago

https://github.com/koba04/react-fiber-resources This repo has collective resources and some nice call stack charts captured in Chrome dev tool.

As it stated React Fiber reconciliation makes many features possible like Suspense (optimising IO bound, avoid unnecessary loading state spinning around) and Concurrent Mode (optimising CPU bound ops with interruptible rendering with sophisticated scheduling, can be paused, resumed or aborted)

These features should be coming in React 18 release. It's clear that React is pushing the boundary of performant UI runtime in a single call stack (thread)