From 40338a9498a889c5f1b2402ecfa25bb25cb57639 Mon Sep 17 00:00:00 2001 From: Thomas Hintz Date: Sun, 2 Aug 2020 11:58:08 -0700 Subject: [PATCH] Chapter one clarifications and edits. --- high-performance-react.org | 178 ++++++++++-------- ...undamentals--building-our-own-react.markua | 61 +++--- 2 files changed, 132 insertions(+), 107 deletions(-) diff --git a/high-performance-react.org b/high-performance-react.org index 7d5382d..172697b 100644 --- a/high-performance-react.org +++ b/high-performance-react.org @@ -6,10 +6,9 @@ '("xelatex -shell-escape -interaction nonstopmode -output-directory %o %f")) #+END_SRC -# #+latex_class: article -# #+latex_class_options: [a4paper,8pt] -# #+latex_header: \usepackage[a4paper,top=2.5cm,bottom=2.5cm,left=1.5cm,right=1.5cm]{geometry} -# #+latex_header:\renewcommand{\baselinestretch}{1.2} +#+latex_class: book +#+latex_class_options: [book,16pt,oneside] +#+latex_header: \usepackage[book,top=2.5cm,bottom=2.5cm,left=2.5cm,right=2.5cm]{geometry} #+TITLE: High-Performance React #+AUTHOR: Thomas Hintz @@ -213,18 +212,18 @@ This is what I'm thinking: ] #+END_SRC -As you can see in, we have a clear mapping from our notation, JSM, to -the original HTML. Our tree is made up of three element arrays. The -first item in the array is the tag, the second is an object containing -the tag's properties, and the third is an array of its children; which -are all made up of the same three element arrays. +As you can see, we have a clear mapping from our notation, JSM, to the +original HTML. Our tree is made up of three element arrays. The first +item in the array is the tag, the second is an object containing the +tag's properties, and the third is an array of its children; which are +all made up of the same three element arrays. The truth is though, if you stare at it long enough, although the mapping is clear, how much fun would it be to read and write that on a consistent basis? I can assure you, it is rather not fun. But it has the advantage of being easy to insert into the DOM. All you need to do is write a simple recursive function that ingests our data structure -and updates the DOM accordingly. We will get back to this. +and updates the DOM accordingly. We will get back to that. So now we have a way to represent a tree of nodes and we (theoretically) have a way to get those nodes into the DOM. But if we @@ -239,11 +238,11 @@ arbitrary JavaScript expressions wherever you want in a node. As you may have realized, that's exactly what the JSX compiler does when it sees curly braces! -There are three main differences between our data structure and the -real one that the JSX compiler outputs: it uses objects instead of -arrays, it inserts calls to React.createElement on children, and -spreads the children instead of containing them in an array. Here is -what real JSX compiler output looks like: +There are three main differences between JSM and the real output of +the JSX compiler: it uses objects instead of arrays, it inserts calls +to React.createElement on children, and spreads the children instead +of containing them in an array. Here is what real JSX compiler output +looks like: # #+NAME: foo # #+CAPTION: foo bar @@ -259,12 +258,12 @@ React.createElement( ); #+END_SRC -As you can see, it is very similar to our markup data-structure and -for the purposes of this book we will use our own simplified -data-structure as it's a bit easier to work with. A JSX compiler also -does some validation and escapes input to prevent cross-site scripting -attacks. In practice though they would behave the same in the ways -that matter to us now. +As you can see, it is very similar to our JSM data-structure and for +the purposes of this book we will use JSM, as it's a bit easier to +work with. A JSX compiler also does some validation and escapes input +to prevent cross-site scripting attacks. In practice though, it would +behave the same in our areas of study and we will keep things simple +by leaving those aspects of the JSX compiler out. So now that we've worked through JSX we're ready to tackle ~createElement~, the next item on our way to building our own React. @@ -273,9 +272,8 @@ So now that we've worked through JSX we're ready to tackle React's ~render~ expects to consume a tree of element objects in a specific, uniform format. ~createElement~ is the method by which we -achieve that objective. ~createElement~ will take as input our -JSX-like notation and output a tree of objects compatible with -~render~. +achieve that objective. ~createElement~ will take as input JSM and +output a tree of objects compatible with ~render~. React expects nodes defined as JavaScript objects that look like this: @@ -295,12 +293,11 @@ That is: an object with two properties: ~type~ and ~props~. The ~props~ property contains all the properties of the node. The node's ~children~ are also considered part of its properties. The full version of React's ~createElement~ includes more properties but they -are unlikely to be relevant to your application's performance or our -version of React here. +are not relevant to our study here. #+BEGIN_SRC javascript function createElement(node) { - // an array: not text, number, or other primitive + // if array (not text, number, or other primitive) if (typeof node === 'object') { const [ tag, props, children ] = node; return { @@ -338,9 +335,9 @@ process of rendering our tree to the DOM! ** 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 next puzzle we -will be solving is the render method. +own React. The next piece is: ~render~. How do we go from our JSM tree +of nodes, to actually displaying something on screen? To do that we +will explore the ~render~ method. The signature for our ~render~ method should be familiar to you: @@ -368,7 +365,10 @@ the pseudocode until we have our own fully functional ~render~ method using the same general algorithm React uses. In our first pass we will focus on the initial render and ignore reconciliation. -TODO note what reconciliation is +#+BEGIN_NOTE +Reconciliation is basically React's "diffing" algorithm. We will be +exploring it after we work out the initial render. +#+END_NOTE #+BEGIN_SRC javascript function render(element, container) { @@ -396,9 +396,9 @@ The ~render~ method starts by creating 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 the keys, setting each property directly. Following that, we render each of the children -by looping over the children and recursively calling ~render~ on each -child with the ~container~ set to the current DOM element (which is -each child's parent). +by looping over them and recursively calling ~render~ on each child +with the ~container~ set to the current DOM element (which is each +child's parent). Now we can go all the way from our JSX-like notation to a rendered tree in the browser's DOM! But so far we can only add things to our @@ -416,14 +416,14 @@ changes. In a lot of ways, it is what makes React, React. Conceptually, the way this works is that React generates a new element tree for every render and compares 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. +tree generated on the previous render. Where it finds differences +between the trees it knows to mutate the DOM state. This is the "tree +diffing" algorithm. -Unfortunately those researching tree diffing in Computer Science have +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 [[https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf][runs in -O(n^3)]]. +use in something like React; as the current best algorithm still [[https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf][runs +in O(n^3)]]. Since an O(n^3) algorithm isn't going to cut it in the real-world, the creators of React instead use a set of heuristics to determine what @@ -446,16 +446,19 @@ In this section we will focus on the first part: differing types. In a later chapter we will discuss and implement the ~key~ prop. 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 React's actual implementation later when we -talk about Fibers. +React uses into our ~render~ method. Our implementation will be very +similar to how React itself does it and we will discuss React's actual +implementation later when we talk about Fibers. Before we get into the code changes that implement the heuristics it is important to remember that React /only/ looks at an element's type, existence, and key. It does not do any other diffing. It does not diff props. It does not diff sub-trees of modified parents. -Here is an overview of the algorithm we will be implementing: +While keeping that in mind, here is an overview of the algorithm we +will be implementing in the ~render~ method. ~element~ is the element +from the current tree and ~prevElement~ is the corresponding element +in the tree from the previous render. #+BEGIN_SRC javascript if (!element && prevElement) @@ -469,22 +472,21 @@ Here is an overview of the algorithm we will be implementing: #+END_SRC 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, their ~render~ methods -will still be invoked. +the element's children. And while it's possible that the children will +have their associated DOM elements reused, their ~render~ methods will +still be invoked. Now, to get started with our render method we must make some -modifications to our previous render method. 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. +modifications to our previous render method. 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 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 element +objects 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~. @@ -510,7 +512,7 @@ rendering but takes an additional argument now: the ~prevElement~. ~prevElement~ 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 +render or if we are rendering a new node or branch of the tree then ~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. @@ -553,21 +555,22 @@ 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 -replaced every time we render, only the elements that change are +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 replaced every time we render, only the elements that change are replaced. -In React, when a new DOM element is appended to the DOM tree, React -would invoke ~componentDidMount~ or ~useEffect~. +In the real React, when a new DOM element is appended to the DOM tree, +React would invoke ~componentDidMount~ or schedule ~useEffect~. 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. Before removing the element, React would invoke -~componentWillUnmount~ and ~useEffect~. +~componentWillUnmount~ and schedule the cleanup function for +~useEffect~. #+BEGIN_SRC javascript function removeDOMElement(prevElement) { @@ -580,7 +583,7 @@ 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~. +~componentWillMount~ or schedule ~useEffect~. #+BEGIN_SRC javascript function createDOMElement(element) { @@ -592,7 +595,7 @@ 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 +course, we filter out the ~children~ prop since we use that elsewhere and it isn't intended to be set directly. #+BEGIN_SRC javascript @@ -601,7 +604,7 @@ function setDOMProps(element, domElement, prevElement) { Object.keys(prevElement.props) .filter((key) => key !== 'children') .forEach((key) => { - domElement[key] = ''; + domElement[key] = ''; // clear prop }); } Object.keys(element.props) @@ -618,7 +621,7 @@ need to be updated or removed. #+end_note #+begin_warning -This algorithm for setting props does not correctly handle events +This algorithm for setting props does not correctly handle events, which must be treated specially. For this exercise that detail is not important though. #+end_warning @@ -650,24 +653,34 @@ function renderChildren(element, domElement, prevElement = { props: { children: 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 +like using a list index for a key. 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 +#+CAPTION: Example of ~renderChildren~ 2nd loop when the 1st element +#+CAPTION: has been removed. In this case the trees for all of the +#+CAPTION: children will be torn down and rebuilt. +| i | child Type | prevChild Type | +|---+------------+----------------| +| 0 | span | div | +| 1 | input | span | +| 2 | - | input | + +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 or hooks are invoked. And invoking those lifecycle methods or hooks, 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. +expensive. So again, if you use a bad key, like the algorithm here +simulates, you'll be hitting a major performance bottleneck since +React will not only be replacing DOM elements in the browser but also +tearing down and rebuilding the trees of child components. ** Fibers @@ -692,6 +705,9 @@ 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 note that unlike class lifecycle events most hooks, like +useEffect + ** Putting it all together Throughout the rest of the book we will be building on and using our diff --git a/manuscript/fundamentals--building-our-own-react.markua b/manuscript/fundamentals--building-our-own-react.markua index 54bbb65..fbf618b 100644 --- a/manuscript/fundamentals--building-our-own-react.markua +++ b/manuscript/fundamentals--building-our-own-react.markua @@ -59,15 +59,15 @@ This is what I'm thinking: ] ``` -As you can see in, we have a clear mapping from our notation, JSM, to the original HTML. Our tree is made up of three element arrays. The first item in the array is the tag, the second is an object containing the tag's properties, and the third is an array of its children; which are all made up of the same three element arrays. +As you can see, we have a clear mapping from our notation, JSM, to the original HTML. Our tree is made up of three element arrays. The first item in the array is the tag, the second is an object containing the tag's properties, and the third is an array of its children; which are all made up of the same three element arrays. -The truth is though, if you stare at it long enough, although the mapping is clear, how much fun would it be to read and write that on a consistent basis? I can assure you, it is rather not fun. But it has the advantage of being easy to insert into the DOM. All you need to do is write a simple recursive function that ingests our data structure and updates the DOM accordingly. We will get back to this. +The truth is though, if you stare at it long enough, although the mapping is clear, how much fun would it be to read and write that on a consistent basis? I can assure you, it is rather not fun. But it has the advantage of being easy to insert into the DOM. All you need to do is write a simple recursive function that ingests our data structure and updates the DOM accordingly. We will get back to that. So now we have a way to represent a tree of nodes and we (theoretically) have a way to get those nodes into the DOM. But if we are being honest with ourselves, while functional, it isn't a pretty notation nor easy to work with. And this is where our object of study enters the scene. JSX is just a notation that a compiler takes as input and outputs in its place a tree of nodes nearly identical to the notation we came up with! And if you look back to our notation you can see that you can easily embed arbitrary JavaScript expressions wherever you want in a node. As you may have realized, that's exactly what the JSX compiler does when it sees curly braces! -There are three main differences between our data structure and the real one that the JSX compiler outputs: it uses objects instead of arrays, it inserts calls to React.createElement on children, and spreads the children instead of containing them in an array. Here is what real JSX compiler output looks like: +There are three main differences between JSM and the real output of the JSX compiler: it uses objects instead of arrays, it inserts calls to React.createElement on children, and spreads the children instead of containing them in an array. Here is what real JSX compiler output looks like: {format: "javascript"} ``` @@ -81,13 +81,13 @@ React.createElement( ); ``` -As you can see, it is very similar to our markup data-structure and for the purposes of this book we will use our own simplified data-structure as it's a bit easier to work with. A JSX compiler also does some validation and escapes input to prevent cross-site scripting attacks. In practice though they would behave the same in the ways that matter to us now. +As you can see, it is very similar to our JSM data-structure and for the purposes of this book we will use JSM, as it's a bit easier to work with. A JSX compiler also does some validation and escapes input to prevent cross-site scripting attacks. In practice though, it would behave the same in our areas of study and we will keep things simple by leaving those aspects of the JSX compiler out. So now that we've worked through JSX we're ready to tackle `createElement`, the next item on our way to building our own React. ## Getting Ready to Render with `createElement` -React's `render` expects to consume a tree of element objects in a specific, uniform format. `createElement` is the method by which we achieve that objective. `createElement` will take as input our JSX-like notation and output a tree of objects compatible with `render`. +React's `render` expects to consume a tree of element objects in a specific, uniform format. `createElement` is the method by which we achieve that objective. `createElement` will take as input JSM and output a tree of objects compatible with `render`. React expects nodes defined as JavaScript objects that look like this: @@ -104,12 +104,12 @@ React expects nodes defined as JavaScript objects that look like this: } ``` -That is: an object with two properties: `type` and `props`. The `props` property contains all the properties of the node. The node's `children` are also considered part of its properties. The full version of React's `createElement` includes more properties but they are unlikely to be relevant to your application's performance or our version of React here. +That is: an object with two properties: `type` and `props`. The `props` property contains all the properties of the node. The node's `children` are also considered part of its properties. The full version of React's `createElement` includes more properties but they are not relevant to our study here. {format: "javascript"} ``` function createElement(node) { - // an array: not text, number, or other primitive + // if array (not text, number, or other primitive) if (typeof node === 'object') { const [ tag, props, children ] = node; return { @@ -138,7 +138,7 @@ That's it. Now we have everything we need to actually begin the process of rende ## 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 next puzzle we will be solving is the render method. +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 JSM tree of nodes, to actually displaying something on screen? To do that we will explore the `render` method. The signature for our `render` method should be familiar to you: @@ -162,7 +162,7 @@ Our DOM element is created first. Then we set the properties, render children el Now that we have an idea of what to build we will work on expanding the pseudocode until we have our own fully functional `render` method using the same general algorithm React uses. In our first pass we will focus on the initial render and ignore reconciliation. -TODO note what reconciliation is +> Reconciliation is basically React's "diffing" algorithm. We will be exploring it after we work out the initial render. {format: "javascript"} ``` @@ -187,7 +187,7 @@ function render(element, container) { } ``` -The `render` method starts by creating 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 the keys, setting each property directly. Following that, we render each of the children by looping over the children and recursively calling `render` on each child with the `container` set to the current DOM element (which is each child's parent). +The `render` method starts by creating 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 the keys, setting each property directly. Following that, we render each of the children by looping over them and recursively calling `render` on each child with the `container` set to the current DOM element (which is each child's parent). Now we can go all the way from our JSX-like notation 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 one more part: reconciliation. @@ -195,9 +195,9 @@ Now we can go all the way from our JSX-like notation to a rendered tree in the b A tale of two trees. These are the two trees that people most often talk about when talking about React's "secret sauce": the virtual DOM and the browser's DOM 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. In a lot of ways, it is what makes React, React. -Conceptually, the way this works is that React generates a new element tree for every render and compares 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. +Conceptually, the way this works is that React generates a new element tree for every render and compares the newly generated tree to the tree generated on the previous render. Where it finds differences between the trees 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^)](https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf). +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 algorithm still [runs in O(n^3^)](https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf). Since an O(n^3^) algorithm isn't going to cut it in the real-world, 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 itself. @@ -208,11 +208,11 @@ According to the [React documentation](https://reactjs.org/docs/reconciliation.h In this section we will focus on the first part: differing types. In a later chapter we will discuss and implement the `key` prop. -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 React's actual implementation later when we talk about Fibers. +The approach we will take here is to integrate the heuristics that React uses into our `render` method. Our implementation will be very similar to how React itself does it and we will discuss React's actual implementation later when we talk about Fibers. Before we get into the code changes that implement the heuristics it is important to remember that React *only* looks at an element's type, existence, and key. It does not do any other diffing. It does not diff props. It does not diff sub-trees of modified parents. -Here is an overview of the algorithm we will be implementing: +While keeping that in mind, here is an overview of the algorithm we will be implementing in the `render` method. `element` is the element from the current tree and `prevElement` is the corresponding element in the tree from the previous render. {format: "javascript"} ``` @@ -226,9 +226,9 @@ else if (element.type !== prevElement.type) // replace dom element, render children ``` -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, their `render` methods will still be invoked. +Notice that in every case, except deletion, we still call `render` on the element's children. And while it's possible that the children will have their associated DOM elements reused, their `render` methods will still be invoked. -Now, to get started with our render method we must make some modifications to our previous render method. 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. +Now, to get started with our render method we must make some modifications to our previous render method. 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 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 element objects 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`. @@ -245,7 +245,7 @@ function render(element, container) { As you can see, the change we made is to move the core of our algorithm into a new function called `render_internal` and pass in the result of our last render to `render_internal`. -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`. `prevElement` 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. +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`. `prevElement` 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 of the tree then `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"} ``` @@ -281,13 +281,13 @@ function render_internal(element, container, prevElement) { } ``` -The only time we shouldn'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 replaced every time we render, only the elements that change are replaced. +The only time we shouldn'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 replaced every time we render, only the elements that change are replaced. -In React, when a new DOM element is appended to the DOM tree, React would invoke `componentDidMount` or `useEffect`. +In the real React, when a new DOM element is appended to the DOM tree, React would invoke `componentDidMount` or schedule `useEffect`. 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. Before removing the element, React would invoke `componentWillUnmount` and `useEffect`. +Removing a DOM element is straightforward; we just `removeChild` on the parent element. Before removing the element, React would invoke `componentWillUnmount` and schedule the cleanup function for `useEffect`. {format: "javascript"} ``` @@ -296,7 +296,7 @@ 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`. +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` or schedule `useEffect`. {format: "javascript"} ``` @@ -307,7 +307,7 @@ 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. +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"} ``` @@ -316,7 +316,7 @@ function setDOMProps(element, domElement, prevElement) { Object.keys(prevElement.props) .filter((key) => key !== 'children') .forEach((key) => { - domElement[key] = ''; + domElement[key] = ''; // clear prop }); } Object.keys(element.props) @@ -329,7 +329,7 @@ function setDOMProps(element, domElement, prevElement) { I> React is more intelligent about only updating or removing props that need to be updated or removed. -W> This algorithm for setting props does not correctly handle events which must be treated specially. For this exercise that detail is not important though. +W> This algorithm for setting props does not correctly handle events, which must be treated specially. For this exercise that detail is not important though. 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 children of the parent element, 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. @@ -350,9 +350,16 @@ function renderChildren(element, domElement, prevElement = { props: { children: } ``` -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. +It's very important to understand the algorithm used here because this is essentially what happens in React when incorrect keys are used, like using a list index for a key. 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 or hooks are invoked. And invoking those lifecycle methods or hooks, 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. +{caption: "children will be torn down and rebuilt."} +| i | child Type | prevChild Type | +|--- |---------- |-------------- | +| 0 | span | div | +| 1 | input | span | +| 2 | - | input | + +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 or hooks are invoked. And invoking those lifecycle methods or hooks, and the whole process of tearing down and building up a component is expensive. So again, if you use a bad key, like the algorithm here simulates, you'll be hitting a major performance bottleneck since React will not only be replacing DOM elements in the browser but also tearing down and rebuilding the trees of child components. ## Fibers @@ -360,6 +367,8 @@ The actual React implementation used to look very similar to what we've gone thr 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 note that unlike class lifecycle events most hooks, like useEffect + ## Putting it all together Throughout the rest of the book we will be building on and using our React implementation so it would be helpful to see it all put together and working. At this point the only thing left to do is to create some components and use them!