Expanding first chapter.

master
Thomas Hintz 4 years ago
parent 6bfcc95f12
commit bde43fe508

1
.gitignore vendored

@ -0,0 +1 @@
*~

@ -454,10 +454,16 @@ Now, to get started with our render method we must make some
modifications to our previous 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
re-render something or if we can re-use DOM elements from the previous
render tree. And last we need to return a tree of elements that can be
used in the next render as a comparison and to reference the DOM
elements that we create. These new elements will have the same
structure as our current elements but we will add two new properties:
~domElement~ and ~parent~. ~domElement~ is the DOM element associated
with our synthetic element and ~parent~ is a reference to the parent
DOM element.
Here we begin by adding a global object that will store our last render
tree, keyed by the ~container~.
#+BEGIN_SRC javascript
@ -470,10 +476,17 @@ function render(element, container) {
}
#+END_SRC
TODO note that we are adding parent and domElement properties.
Now that we have a way to see what we rendered last time we can go
ahead and update our render method with the heuristics.
Now that we have stored our last render tree we can go ahead and
update our render method with the heuristics for reusing the DOM
elements. We name it ~render_internal~ because it is what controls the
rendering but takes an additional argument now: the
~prevElement~. ~preElement~ is a reference to the corresponding
~element~ from the previous render and contains a reference to its
associated DOM element and parent DOM element. If it's the first
render or if we are rendering a new node or branch to the tree than
~prevElement~ will be ~undefined~. If, however, ~element~ is
~undefined~ and ~prevElement~ is defined then we know we need to
delete a node that previously existed.
#+BEGIN_SRC javascript
function render_internal(element, container, prevElement) {
@ -485,7 +498,7 @@ function render_internal(element, container, prevElement) {
domElement = createDOMElement(element);
} else if (element.type === prevElement.type) {
domElement = prevElement.domElement;
} else {
} else { // types don't match
removeDOMElement(prevElement);
domElement = createDOMElement(element);
}
@ -508,12 +521,38 @@ function render_internal(element, container, prevElement) {
}
#+END_SRC
The only time we can't set DOM properties on our element and render
its children is when we are deleting an existing DOM element. We use
this observation to group the calls for ~setDOMProps~ and
~renderChildren~. Choosing when to append a new DOM element to the
container is also part of the heuristics. If we can reuse an existing
DOM element then we do but if the element type has changed or if there
was no corresponding existing DOM element then and only then do we
append a new DOM element. This ensures the actual DOM tree isn't being
swapped out every time we render, only the elements that change
are. When a new DOM element is appended to the tree React would invoke
~componentDidMount~.
Next up we'll go through all the auxiliary methods that complete the
implementation.
Removing a DOM element is straightforward; we just ~removeChild~ on
the parent element. In React, before removing the element, it invoke
~componentWillUnmount~.
#+BEGIN_SRC javascript
function removeDOMElement(prevElement) {
prevElement.parent.removeChild(prevElement.domElement);
}
#+END_SRC
In creating a new DOM element we just need to branch if we are
creating a text element since the browser API differs slightly. We
also populate the text element's value as the API requires the first
argument to be specified even though later on when we set props we
will set it again. This is where React would invoke
~componentWillMount~.
#+BEGIN_SRC javascript
function createDOMElement(element) {
return element.type === 'TEXT' ?
@ -522,6 +561,11 @@ function createDOMElement(element) {
}
#+END_SRC
To set the props on an element we first clear all the existing props
and then loop through the current props, setting them accordingly. Of
course we filter out the ~children~ prop since we use that elsewhere
and it isn't intended to be set directly.
#+BEGIN_SRC javascript
function setDOMProps(element, domElement, prevElement) {
if (prevElement) {
@ -539,6 +583,21 @@ function setDOMProps(element, domElement, prevElement) {
}
#+END_SRC
TODO note that React is more intelligent settings props, it only
updates the ones that need to update
TODO this algorithm doesn't correctly handle event handler props but
we're ignoring that for simplicity
For rendering children we use two loops. The first loop removes any
elements that are no longer being used. This would happen when the
number of children is decreased. The second loop starts at the first
child and then iterates through all of the current children calling
~render_internal~ on each child. When ~render_internal~ is called the
corresponding previous element in that position is passed to
~render_internal~ or ~undefined~ if there is no corresponding element,
like when the list of children has grown.
#+BEGIN_SRC javascript
function renderChildren(element, domElement, prevElement = { props: { children: [] }}) {
const elementLen = element.props.children.length;
@ -555,11 +614,52 @@ function renderChildren(element, domElement, prevElement = { props: { children:
}
#+END_SRC
TODO don't figure event handlers are handled specially
** Commit Phase
It's very important to understand the algorithm used here because this
is essentially what happens in React when incorrect keys are used,
like a list index. And this is why keys are so critical to high
performance (and correct) React code. For example, in our algorithm
here, if you removed an item from the front of the list you may cause
every element in the list to be created anew in the DOM if the types
no longer match up. Later on, in the chapter on keys, we will update
this algorithm to incorporate keys. It's actually only a minor
difference in determining which ~child~ gets paired with which
~prevChild~. Otherwise this is effectively the same algorithm React
uses when rendering lists of children.
There are a few things to note here. First it is important to pay
attention to when React will be removing a DOM element from the tree
and adding a new one as this is when the related lifecycle events are
invoked. And invoking those lifecycle methods, and the whole process
of tearing down and building up a component is expensive. So again you
can see how a bad key would lead to another performance bottleneck
since React will be doing this on all or many of the elements in a
list frequently.
** Fibers
The actual React implementation used to look very similar to what
we've gone through so far but with React 16 this has changed
dramatically with the introduction of Fibers. Fibers are a name that
React gives to discrete units of work. And the React reconciliation
algorithm was changed to be based on small units of work instead of
one large, potentially long-running call to ~render~. This means that
React is now able to process just part of the render phase, pause to
let the browser take care of other things, and resume again. This is
the underlying change the enables the experimental Concurrent Mode.
But even with such a large change, the underlying algorithms for
deciding how and when to render components is the same. And when not
running in Concurrent Mode the effect is still the same as React does
the render phase in one block still. So using a simplified
interpretation that doesn't include all the complexities of breaking
up the process in to chunks enables us to see more clearly how the
process as a whole works. At this point bottlenecks are much more
likely to occur from the underlying algorithms and not from the Fiber
specific details. In the chapter on Concurrent Mode we will go in to
this more.
TODO maybe a graphic summarizing the heuristics?
* Rendering Model
:PROPERTIES:
:EXPORT_FILE_NAME: manuscript/rendering-model.markua

@ -232,9 +232,9 @@ else if (element.type !== prevElement.type)
Notice that in every case, except deletion, we still call `render` on the element's children. While its possible that the children will be able to reuse their associated DOM elements, `render` will still be run on them.
Now, to get started with our render method we must make some modifications to our previous 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.
Now, to get started with our render method we must make some modifications to our previous 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 DOM elements from the previous render tree. And last we need to return a tree of elements that can be used in the next render as a comparison and to reference the DOM elements that we create. These new elements will have the same structure as our current elements but we will add two new properties: `domElement` and `parent`. `domElement` is the DOM element associated with our synthetic element and `parent` is a reference to the parent DOM element.
Here we are adding a global object that will store our last render tree, keyed by the `container`.
Here we begin by adding a global object that will store our last render tree, keyed by the `container`.
{format: "javascript"}
```
@ -247,9 +247,7 @@ function render(element, container) {
}
```
TODO note that we are adding parent and domElement properties.
Now that we have a way to see what we rendered last time we can go ahead and update our render method with the heuristics.
Now that we have stored our last render tree we can go ahead and update our render method with the heuristics for reusing the DOM elements. We name it `render_internal` because it is what controls the rendering but takes an additional argument now: the `prevElement`. `preElement` is a reference to the corresponding `element` from the previous render and contains a reference to its associated DOM element and parent DOM element. If it's the first render or if we are rendering a new node or branch to the tree than `prevElement` will be `undefined`. If, however, `element` is `undefined` and `prevElement` is defined then we know we need to delete a node that previously existed.
{format: "javascript"}
```
@ -262,7 +260,7 @@ function render_internal(element, container, prevElement) {
domElement = createDOMElement(element);
} else if (element.type === prevElement.type) {
domElement = prevElement.domElement;
} else {
} else { // types don't match
removeDOMElement(prevElement);
domElement = createDOMElement(element);
}
@ -285,6 +283,12 @@ function render_internal(element, container, prevElement) {
}
```
The only time we can't set DOM properties on our element and render its children is when we are deleting an existing DOM element. We use this observation to group the calls for `setDOMProps` and `renderChildren`. Choosing when to append a new DOM element to the container is also part of the heuristics. If we can reuse an existing DOM element then we do but if the element type has changed or if there was no corresponding existing DOM element then and only then do we append a new DOM element. This ensures the actual DOM tree isn't being swapped out every time we render, only the elements that change are. When a new DOM element is appended to the tree React would invoke `componentDidMount`.
Next up we'll go through all the auxiliary methods that complete the implementation.
Removing a DOM element is straightforward; we just `removeChild` on the parent element. In React, before removing the element, it invoke `componentWillUnmount`.
{format: "javascript"}
```
function removeDOMElement(prevElement) {
@ -292,6 +296,8 @@ function removeDOMElement(prevElement) {
}
```
In creating a new DOM element we just need to branch if we are creating a text element since the browser API differs slightly. We also populate the text element's value as the API requires the first argument to be specified even though later on when we set props we will set it again. This is where React would invoke `componentWillMount`.
{format: "javascript"}
```
function createDOMElement(element) {
@ -301,6 +307,8 @@ function createDOMElement(element) {
}
```
To set the props on an element we first clear all the existing props and then loop through the current props, setting them accordingly. Of course we filter out the `children` prop since we use that elsewhere and it isn't intended to be set directly.
{format: "javascript"}
```
function setDOMProps(element, domElement, prevElement) {
@ -319,6 +327,12 @@ function setDOMProps(element, domElement, prevElement) {
}
```
TODO note that React is more intelligent settings props, it only updates the ones that need to update
TODO this algorithm doesn't correctly handle event handler props but we're ignoring that for simplicity
For rendering children we use two loops. The first loop removes any elements that are no longer being used. This would happen when the number of children is decreased. The second loop starts at the first child and then iterates through all of the current children calling `render_internal` on each child. When `render_internal` is called the corresponding previous element in that position is passed to `render_internal` or `undefined` if there is no corresponding element, like when the list of children has grown.
{format: "javascript"}
```
function renderChildren(element, domElement, prevElement = { props: { children: [] }}) {
@ -336,10 +350,16 @@ function renderChildren(element, domElement, prevElement = { props: { children:
}
```
TODO don't figure event handlers are handled specially
It's very important to understand the algorithm used here because this is essentially what happens in React when incorrect keys are used, like a list index. And this is why keys are so critical to high performance (and correct) React code. For example, in our algorithm here, if you removed an item from the front of the list you may cause every element in the list to be created anew in the DOM if the types no longer match up. Later on, in the chapter on keys, we will update this algorithm to incorporate keys. It's actually only a minor difference in determining which `child` gets paired with which `prevChild`. Otherwise this is effectively the same algorithm React uses when rendering lists of children.
## Commit Phase
There are a few things to note here. First it is important to pay attention to when React will be removing a DOM element from the tree and adding a new one as this is when the related lifecycle events are invoked. And invoking those lifecycle methods, and the whole process of tearing down and building up a component is expensive. So again you can see how a bad key would lead to another performance bottleneck since React will be doing this on all or many of the elements in a list frequently.
## Fibers
The actual React implementation used to look very similar to what we've gone through so far but with React 16 this has changed dramatically with the introduction of Fibers. Fibers are a name that React gives to discrete units of work. And the React reconciliation algorithm was changed to be based on small units of work instead of one large, potentially long-running call to `render`. This means that React is now able to process just part of the render phase, pause to let the browser take care of other things, and resume again. This is the underlying change the enables the experimental Concurrent Mode.
But even with such a large change, the underlying algorithms for deciding how and when to render components is the same. And when not running in Concurrent Mode the effect is still the same as React does the render phase in one block still. So using a simplified interpretation that doesn't include all the complexities of breaking up the process in to chunks enables us to see more clearly how the process as a whole works. At this point bottlenecks are much more likely to occur from the underlying algorithms and not from the Fiber specific details. In the chapter on Concurrent Mode we will go in to this more.
TODO maybe a graphic summarizing the heuristics?

Loading…
Cancel
Save