cyan33 / learn-react-source-code

Build react from scratch (code + blog)
208 stars 23 forks source link

Day 2 - Mounting & `createElement` #2

Closed cyan33 closed 6 years ago

cyan33 commented 6 years ago

Dive Into A Minimum Implementation

We've already done pretty much preparation before diving into the code. First, we need to find a point to get started with. Core is just core. So let's start with the most used and important top-level APIs, React.createElement and ReactDOM.render.

Note We're gonna first try to build a simple version of React, and dive into more details. Much of this part refers to @zpao's Building React From Scratch. We might come back later to add more features on top of it. But it's absolutely not intuitive to try to finish all stuff at one time. That's not engineering.

React.createElement(component, props, ...children)

We all know that the JSX compiles html-like code to React.createElement(name, config, children). Like you've read in the JSX advanced guide. We could have these two mapping as below:

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

is the same as

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

Essentially, when you're returning a JSX fragment, you're actually returning a element tree object, describing the hierarchy of the DOM. React use this method to recursively go down to the very end where all the component type are all DOM nodes and finally form a nested DOM representative. But createElement per se, is atomic (not recursive).

function createElement(type, config, ...children) {
  let props = Object.assign({}, config);
  props.children = [...children]

  // React Features not supported:
  // - keys
  // - refs
  // - defaultProps (usually set here)

  return {
    type,
    props,
  };
}

So if you use the last example, when we return that JSX fragment, we are actually calling:

return createElement(MyButton, { color: 'blue', shadowSize: 2 }, 'Click Me')

which is also:

return {
  type: MyButton,
  props: { color: 'blue', shadowSize: 2 },
  children: ['Click Me']
}

Mount

Mounting is the process of convert a react component instance to a DOM node (for the first time, i.e. initial render).

When talking about a React component, there are three kinds of possibilities. The component could be native type of node, like div, span, or customized React component with capitalized letter, or it could be as simple as a string (text node).

function mount(component, node) {
  // mark this node as root, so that next time we could possibly use `update` instead of `mount`
  node[DATA_KEY] = rootId;

  if (typeof component === 'string' || typeof component === 'number') {
    mountTextNode(component, node);
  } else if (typeof component.type === 'function') {
    mountComposite(component, node);
  } else {
    mountHost(component, node);
  }

  rootId++;
}

function mountTextNode(text, node) {
  const textNode = document.createTextNode(text);
  node.appendChild(textNode);
}

function mountComposite(component, node) {
  // go one level deeper each time until it's a native node
  const deeperNode = component.type(component.props);
  // delegate to mount
  return mount(deeperNode, node);
}

function mountHost(component, node) {
  const nativeNode = document.createElement(component.type)
  const children = component.props.children

  Object.keys(component.props).forEach((propName) => {
    if (propName !== 'children') {
      nativeNode.setAttribute(propName, component.props[propName])
    }
  })

  node.appendChild(nativeNode)

  children.filter((child) => !!child).forEach((child) => {
    mount(child, nativeNode)
  })
}

What've We Achieved So Far

In this post, we talked mainly about two things:

  1. How a React component is converted and rendered to a DOM node, which is the mounting process.
  2. How to create a React element, which is what a React component returns, and how this simplifies the syntax and the building process, so that we don've have to write an react element object like this:
return {
  type: 'div',
  props: { className: 'some-class' },
  children: {
    type: 'button'
    props: { backgroundColor: 'black' },
    children: 'Click me'
  }
}

These two processes are highly related, and each do their job clearly. Now with these two functions, you could already write a test like this (with the JSX syntax):

function BigButton({ color, text }) {
  return (
    <div>
      <button style={{ backgroundColor: color }}>
        {text}
      <button>
    </div>
  )
}

const App = function() {
  return (
    <div>
      Hello World
      <BigButton color="#aa234d" text="Click Me">
    </div>
  )
}

mount(<App />, document.getElementById('root'))

Tip I suggest you go through this example in your heart and say it out each of the process to make yourself completely understand the mounting process.