cyan33 / learn-react-source-code

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

Day 4 - Component #4

Closed cyan33 closed 6 years ago

cyan33 commented 6 years ago

Until now, we've covered mounting, the React element tree, etc. They are parts of the foundations of React. Another important concept, is the React component class. It's only by extending the React.Component, can you take advantage of the setState function and the life cycle hooks built inside React. So in this post, we are gonna cover three sub-topics:

  1. how the component class gets rendered
  2. setState
  3. life cycle hooks

How the Component Class Gets Mounted and Rendered

When you create a class and have it extend from React.Component, there is no difference from extending a normal parent class. You just extend a lot of parent methods, of which are render, setState, and the life cycle hooks.

As we know, a react component class requires at least a render method. So, we could start from here:

class Component {
  constructor(props) {
    if (!this.render) {
      throw Error('a component class must at least have a render method')
    }
    this.props = props
  }
}

Now we should add a mountComponent method so that it could be rendered to the screen the first time. But inside mountComponent, we're gonna call a method recursively (this method is very similar as mount, except it doesn't append to a node directly but return the markup of the element tree, we'll come back later) until the tree is all about native DOM nodes.

The method mountComponent in the class should be:

mountComponent() {
    let renderedElement = this.render();
    // assume the wrapper is also a component class for now
    let renderedComponent = new renderedElement.type();
    return renderedComponent
  }

Now we could update the mountComposite function as:

function mountComposite(component, type) {
  let componentNode;
  if (isClass(component.type)) {
    const component = new component.type(component.props)
    componentNode = component.mountComponent()
  } else {
    componentNode = component.type(component.props)
  }

  // delegate to mount one level deeper
  mount(componentNode, node)
}

Now, we are able to support 4 types of react components in the mounting process, which are:

  1. text node
  2. native DOM node
  3. function component
  4. class component

Refactor and Decouple

Let's re-think about the mounting process.

We call mount in this way:

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

The first argument it takes is an element object, if you still remember what createElement works. And it finally use this element to create the according component, and then generate the real DOM node and append it to '#root'. It could be be separated into the following sub-processes:

element object => component => real DOM node

When creating the according component, we use the if statement to judge what kinds of component it is. But on second thought though, on a very high level like mount, we should use as few as function polymorphism and if statement as possible, but create a generic class for each of them and expose the same method API like initiateComponent, and mountComponent. So before moving on, let's do some refactoring.

We want to create a separate class for each type of the components, and let themselves take charge of creating their corresponding DOM node, so that the polymorphism becomes class-based, and the flow more clear: