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