cyan33 / learn-react-source-code

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

Day 3 - Mounting (Contd) & Updating Components #3

Closed cyan33 closed 6 years ago

cyan33 commented 6 years ago

Mounting Contd.

You've probably noticed that in the last post we wrote:

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

which is very similar to what you are doing in React:

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

Clearly we took a shortcut in the former one. The mounting process is expensive. It's quite like building everything from scratch. We do not want to do that in each render. In react, the real render function looks something like this:

Node Right now, the render that we've been talking about is the ReactDOM.render, not the render function that lives in a React component.

function render(element, node) {
  // First check if we've already rendered into this node.
  // If so, we'll be doing an update.
  // Otherwise we'll assume this is an initial render.
  if (isRoot(node)) {
    update(element, node);
  } else {
    mount(element, node);
  }
}

Like the comment said, we check if it's a root node. If so, we are updating them, instead of mounting from scratch again.

How is the isRoot function implemented? It's quite simple, inside React, we have:

const ROOT_KEY = 'a_unique_key_react_Id';
let rootID = 1;

// Used to track root instances.
const instancesByRootID = {};

and in every mounting process(mount(component, node)), we set

node[ROOT_KEY] = rootID;
instancesByRootID[rootID] = instance

and rootID++ when the mounting ends. They help React keep track of the mounting process.

Update

We already know how mount works. It's just the mapping from a React element to the real DOM instances. In render, however, we also have update when the node has already been rendered once. So it's more cheap. Updating is just re-calculating the props and re-render. Remember the [Reconciliation]() post that we talked about in Day1, about when to update, and when to unmount and re-mount? That's gonna help you understand this part better.

function update(element, node) {
  // Find the internal react instance and update it
  let id = node.dataset[ROOT_KEY];

  let instance = instancesByRootID[id];

  if (shouldUpdateComponent(instance, element)) {
    // use current instance to update the element
    // it's a method in the `Component` class, we'll get back to this in next post
    // right now, just know that the instance gets updated with the new props
    instance.updateComponent(element)
  } else {
    // Unmount and then mount the new one
    unmountComponentAtNode(node);
    mount(element, node);
  }
}

function shouldUpdateComponent(instance, element) {
  // shortcut beforehand if it's not even the same time any more
  return instance.type === element.type
}

function unmountComponentAtNode(node) {
  let id = node.dataset[ROOT_KEY];
  let instance = instancesByRootID[id];

  instance.unmountComponent();
  delete instancesByRootID[id];

  // Reset the DOM node
  node.innerHTML = '';
  delete node.dataset[ROOT_KEY];
}

What We've Achieved So Far

We've covered most of the things you need to know with respect to the render part. But in this part you've already seen many important concepts that React uses to build up its whole architecture, and one of them is the react component element tree, and the JSX syntax which is used to create that.

You may notice that currently in the mount function we only support functional component. Because we directly call:

// in mountComposite()
const newNode = component.type(component.props)

What if the component is not a function, but a class instead? And where does the internal state go? What happens when you create a class that extends React.Component? For that we'll discuss in the next post. Please do take some time to stop and review what we've covered so far. No hurry.