Adding to Render.

master
Thomas Hintz 4 years ago
parent ecb1d96dc8
commit f3a483fe02

@ -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.

Loading…
Cancel
Save