Adding to Render.
This commit is contained in:
@@ -53,6 +53,9 @@ technology changes and that is no less true for this book. But what I
|
||||
hope you learn is not just the technical details but more importantly
|
||||
the method for writing high-performance code. The API might change but
|
||||
the method will remain the same.
|
||||
|
||||
TODO note that the book references React-DOM but the algorithms should
|
||||
generally apply to all React implementations.
|
||||
* Mini React
|
||||
Baking bread. When I first began to learn how to bake bread the recipe
|
||||
told me what to do. It listed some ingredients and told me how to
|
||||
@@ -288,13 +291,216 @@ elements. The first part tests whether ~node~ is a complex node
|
||||
on the input node. It recursively calls ~createElement~ to generate an
|
||||
array of children elements. If the node is not complex then we
|
||||
generate an element of type 'TEXT' which we use for all primitives
|
||||
like strings and numbers.
|
||||
like strings and numbers. We call the output of ~createElement~ a tree
|
||||
of ~elements~ (surprise).
|
||||
|
||||
That's it. Now we have everything we need to actually begin the
|
||||
process of rendering our tree to the DOM!
|
||||
|
||||
*** Reconciliation
|
||||
A tale of two trees.
|
||||
** Render
|
||||
|
||||
There are now only two major puzzles remaining in our quest for our
|
||||
own React. The next piece is: ~render~: how do we go from our tree of
|
||||
nodes to actually displaying something on screen?
|
||||
|
||||
The signature for our ~render~ method is very simple and will be
|
||||
familiar to you:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
function render(element, container)
|
||||
#+END_SRC
|
||||
|
||||
Doing the initial render on a tree of elements is quite simple. In
|
||||
psuedocode it looks like this:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
function render(element, container) {
|
||||
const domElement = createDOMElement(element);
|
||||
setProps(element, domElement);
|
||||
renderChildren(element, domElement);
|
||||
container.appendChild(domElement);
|
||||
#+END_SRC
|
||||
|
||||
Because the browser APIs for text elements are different than for generic
|
||||
DOM elements and because text elements can't have children we will
|
||||
split up the process in to two methods: ~renderTextElement~ and
|
||||
~renderDOMElement~.
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
function render(element, container) {
|
||||
if (element.type === 'TEXT') {
|
||||
renderTextElement(element, container);
|
||||
} else {
|
||||
renderDOMElement(element, container);
|
||||
}
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
First, we'll look at ~renterTextElement~, which is the simpler of the
|
||||
two.
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
function renderTextElement(element, container) {
|
||||
return container.appendChild(
|
||||
document.createTextNode(element.props.nodeValue));
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
~renderTextElement~ just creates a DOM ~TextNode~ and appends it to
|
||||
the container.
|
||||
|
||||
Next, we look at renderDOMElement which must also set properties on
|
||||
the newly created DOM element and render any children.
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
function renderDOMElement(element, container) {
|
||||
const { type, props } = element;
|
||||
|
||||
// create the DOM element
|
||||
const domElement = document.createElement(type);
|
||||
|
||||
// set its properties
|
||||
Object.keys(props)
|
||||
.filter((key) => key !== 'children')
|
||||
.forEach((key) => {
|
||||
domElement[key] = props[key];
|
||||
});
|
||||
|
||||
// render its children
|
||||
props.children.forEach((child) => render(child, domElement));
|
||||
|
||||
// add our tree to the DOM!
|
||||
container.appendChild(domElement);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
To start with we create the DOM element. Then we need to set its
|
||||
properties. To do this we first need to filter out the ~children~
|
||||
property and then we simply loop over they keys setting each property
|
||||
directly. Then we render each of the children by looping over the
|
||||
children recursively calling ~render~ on each with the ~container~ set
|
||||
to the current DOM element (which is each child's parent).
|
||||
|
||||
Now we can go all the way from JSX to a rendered tree in the browser's
|
||||
DOM! But so far we can only add things to our tree. To be able to
|
||||
remove and modify the tree we need two more parts: reconciliation and
|
||||
the commit phase.
|
||||
|
||||
** Reconciliation
|
||||
A tale of two trees. These are the two trees that people most often
|
||||
talk about when talking about React's "secret sauce": the VDOM or
|
||||
virtual DOM and the current render tree. This idea is what originally
|
||||
set React apart. React's reconciliation is what allows you to program
|
||||
declaratively. Reconciliation is what makes it so we no longer have to
|
||||
manually update and modify the DOM whenever our own internal state
|
||||
changes and in a lot of ways is that makes React, React.
|
||||
|
||||
Conceptually the way this works is that React generates a new element
|
||||
tree for every render and compares to the newly generated tree to the
|
||||
tree generated on the previous render. Where it finds differences in
|
||||
the tree it knows to mutate the DOM state. This is the "tree diffing"
|
||||
algorithm.
|
||||
|
||||
Unfortunately those researching tree diffing in Computer Science have
|
||||
not yet produced a generic algorithm with sufficient performance for
|
||||
use in something like React as the current best still runs in
|
||||
O(n^3). This leads to the largest performance related aspect in all of
|
||||
React.
|
||||
|
||||
Since an O(n^3) algorithm isn't going to cut it the creators of React
|
||||
instead use a set of heuristics to determine what parts of the tree
|
||||
have changed. Understanding how the React tree diffing algorithm works
|
||||
in general and the heuristics currently in use can help immensely in
|
||||
detecting and fixing React performance bottlenecks. And beyond that it
|
||||
can help one's understanding of some of React's quirks and usage. Even
|
||||
though this algorithm is internal to React and can be changed anytime
|
||||
its details have leaked out in some ways and are overall unlikely to
|
||||
change in major ways without larger changes to React.
|
||||
|
||||
TODO some kind of call-out for big deal
|
||||
|
||||
TODO https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
|
||||
|
||||
The approach we will take here is to integrate the heuristics that
|
||||
React uses into our render method. This is similar to how React itself
|
||||
does it and we will discuss that later when we talk about Fibers.
|
||||
|
||||
To do this we must make some modifications to our render
|
||||
methods. First, we need to be able to store and retrieve the previous
|
||||
render tree. Then we need to add code to compare parts of the tree to
|
||||
decide if we need to re-render something or if we can re-use it from
|
||||
the previous render tree.
|
||||
|
||||
Here we are adding a global object that will store our last render
|
||||
tree keyed by the container.
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
const renderTrees = {};
|
||||
function render(element, container) {
|
||||
const tree =
|
||||
render_internal(element, container, renderTrees[container]);
|
||||
// render complete, store the updated tree
|
||||
renderTrees[container] = tree;
|
||||
}
|
||||
|
||||
function render_internal(element, container, prevElement) {
|
||||
if (element.type === 'TEXT') {
|
||||
return renderTextElement(element, container, prevElement);
|
||||
} else {
|
||||
return renderDOMElement(element, container, prevElement);
|
||||
}
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
TODO psuedo-code for heuristics
|
||||
#+BEGIN_SRC javascript
|
||||
if (!element && prevElement)
|
||||
// delete dom element
|
||||
else if (element && !prevElement)
|
||||
// add new dom element
|
||||
else if (element.type === prevElement.type)
|
||||
// update dom element
|
||||
#+END_SRC
|
||||
|
||||
Now that we have a way to see what we rendered last time we can go
|
||||
ahead and update our render methods with the heuristics.
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
function renderTextElement(element, container, prevElement) {
|
||||
const { nodeValue } = element.props;
|
||||
let domElement;
|
||||
if (element && prevElement &&
|
||||
element.type === prevElement.type &&
|
||||
prevElement.props.nodeValue &&
|
||||
nodeValue !== prevElement.props.nodeValue) {
|
||||
// types match but values don't; update
|
||||
prevElement.domElement.nodeValue = nodeValue;
|
||||
domElement = prevElement.domElement;
|
||||
} else {
|
||||
if (element && prevElement &&
|
||||
element.type !== prevElement.type) {
|
||||
// element types don't match so remove & append
|
||||
prevElement.parent.removeChild(prevElement.domElement);
|
||||
} else if () {
|
||||
// TODO delete node
|
||||
}
|
||||
// new type or new text node
|
||||
domElement =
|
||||
container.appendChild(document.createTextNode(nodeValue));
|
||||
}
|
||||
return {
|
||||
domElement: domElement,
|
||||
parent: container,
|
||||
...element
|
||||
};
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
TODO don't figure event handlers are handled specially
|
||||
|
||||
** Commit Phase
|
||||
|
||||
** Fibers
|
||||
* Rendering Model
|
||||
React calls shouldComponentUpdate to know if it should re-render the
|
||||
component. by default it returns true.
|
||||
|
||||
Reference in New Issue
Block a user