diff --git a/foundations-high-performance-react.org b/foundations-high-performance-react.org index b3fe3c6..2efb4ac 100644 --- a/foundations-high-performance-react.org +++ b/foundations-high-performance-react.org @@ -12,6 +12,7 @@ #+TITLE: Foundations of High-Performance React Applications #+AUTHOR: Thomas Hintz +#+EXCLUDE_TAGS: noexport #+startup: indent #+tags: noexport sample frontmatter mainmatter backmatter @@ -196,7 +197,8 @@ This is what I'm thinking: #+BEGIN_SRC javascript ['div', { 'className': 'header' }, [['h1', {}, ['Hello']], - ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] + ['input', { 'type': 'submit', 'disabled': 'disabled' }, + []] ] ] #+END_SRC @@ -345,10 +347,10 @@ focusing on the initial render. In pseudocode it looks like this: #+BEGIN_SRC javascript function render(element, container) { - const domElement = createDOMElement(element); - setProps(element, domElement); - renderChildren(element, domElement); - container.appendChild(domElement); + const domElement = createDOMElement(element); + setProps(element, domElement); + renderChildren(element, domElement); + container.appendChild(domElement); #+END_SRC Our DOM element is created first. Then we set the properties, render @@ -367,23 +369,24 @@ exploring it after we work out the initial render. #+BEGIN_SRC javascript function render(element, container) { - const { type, props } = element; + const { type, props } = element; - // create the DOM element - const domElement = type === 'TEXT' ? - document.createTextNode(props.nodeValue) : - document.createElement(type); + // create the DOM element + const domElement = type === 'TEXT' ? + document.createTextNode(props.nodeValue) : + document.createElement(type); - // set its properties - Object.keys(props) - .filter((key) => key !== 'children') - .forEach((key) => domElement[key] = props[key]); + // 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)); + // render its children + props.children.forEach((child) => + render(child, domElement)); - // add our tree to the DOM! - container.appendChild(domElement); + // add our tree to the DOM! + container.appendChild(domElement); } #+END_SRC @@ -464,14 +467,14 @@ from the current tree and ~prevElement~ is the corresponding element in the tree from the previous render. #+BEGIN_SRC javascript - if (!element && prevElement) - // delete dom element - else if (element && !prevElement) - // add new dom element, render children - else if (element.type === prevElement.type) - // update dom element, render children - else if (element.type !== prevElement.type) - // replace dom element, render children +if (!element && prevElement) + // delete dom element +else if (element && !prevElement) + // add new dom element, render children +else if (element.type === prevElement.type) + // update dom element, render children +else if (element.type !== prevElement.type) + // replace dom element, render children #+END_SRC Notice that in every case, except deletion, we still call ~render~ on @@ -497,10 +500,10 @@ 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; + const tree = + render_internal(element, container, renderTrees[container]); + // render complete, store the updated tree + renderTrees[container] = tree; } #+END_SRC @@ -522,34 +525,34 @@ delete a node that previously existed. #+BEGIN_SRC javascript function render_internal(element, container, prevElement) { - let domElement, children; - if (!element && prevElement) { - removeDOMElement(prevElement); - return; - } else if (element && !prevElement) { - domElement = createDOMElement(element); - } else if (element.type === prevElement.type) { - domElement = prevElement.domElement; - } else { // types don't match - removeDOMElement(prevElement); - domElement = createDOMElement(element); - } - setDOMProps(element, domElement, prevElement); - children = renderChildren(element, domElement, prevElement); - - if (!prevElement || domElement !== prevElement.domElement) { - container.appendChild(domElement); - } - - return { - domElement: domElement, - parent: container, - type: element.type, - props: { - ...element.props, - children: children - } - }; + let domElement, children; + if (!element && prevElement) { + removeDOMElement(prevElement); + return; + } else if (element && !prevElement) { + domElement = createDOMElement(element); + } else if (element.type === prevElement.type) { + domElement = prevElement.domElement; + } else { // types don't match + removeDOMElement(prevElement); + domElement = createDOMElement(element); + } + setDOMProps(element, domElement, prevElement); + children = renderChildren(element, domElement, prevElement); + + if (!prevElement || domElement !== prevElement.domElement) { + container.appendChild(domElement); + } + + return { + domElement: domElement, + parent: container, + type: element.type, + props: { + ...element.props, + children: children + } + }; } #+END_SRC @@ -577,7 +580,7 @@ the parent element. Before removing the element, React would invoke #+BEGIN_SRC javascript function removeDOMElement(prevElement) { - prevElement.parent.removeChild(prevElement.domElement); + prevElement.parent.removeChild(prevElement.domElement); } #+END_SRC @@ -590,9 +593,9 @@ will set it again. This is where React would invoke #+BEGIN_SRC javascript function createDOMElement(element) { - return element.type === 'TEXT' ? - document.createTextNode(element.props.nodeValue) : - document.createElement(element.type); + return element.type === 'TEXT' ? + document.createTextNode(element.props.nodeValue) : + document.createElement(element.type); } #+END_SRC @@ -603,18 +606,18 @@ and it isn't intended to be set directly. #+BEGIN_SRC javascript function setDOMProps(element, domElement, prevElement) { - if (prevElement) { - Object.keys(prevElement.props) - .filter((key) => key !== 'children') - .forEach((key) => { - domElement[key] = ''; // clear prop - }); - } - Object.keys(element.props) - .filter((key) => key !== 'children') - .forEach((key) => { - domElement[key] = element.props[key]; - }); + if (prevElement) { + Object.keys(prevElement.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = ''; // clear prop + }); + } + Object.keys(element.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = element.props[key]; + }); } #+END_SRC @@ -640,17 +643,17 @@ 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; - const prevElementLen = prevElement.props.children.length; - // remove now unused elements - for (let i = elementLen; i < prevElementLen - elementLen; i++) { - removeDOMElement(element.props.children[i]); - } - // render existing and new elements - return element.props.children.map((child, i) => { - const prevChild = i < prevElementLen ? prevElement.props.children[i] : undefined; - return render_internal(child, domElement, prevChild); - }); + const elementLen = element.props.children.length; + const prevElementLen = prevElement.props.children.length; + // remove now unused elements + for (let i = elementLen; i < prevElementLen - elementLen; i++) { + removeDOMElement(element.props.children[i]); + } + // render existing and new elements + return element.props.children.map((child, i) => { + const prevChild = i < prevElementLen ? prevElement.props.children[i] : undefined; + return render_internal(child, domElement, prevChild); + }); } #+END_SRC @@ -719,19 +722,19 @@ At this point the only thing left to do is to create some components and use them! #+BEGIN_SRC javascript - const SayNow = ({ dateTime }) => { - return ['h1', {}, [`It is: ${dateTime}`]]; - }; - - const App = () => { - return ['div', { 'className': 'header' }, - [SayNow({ dateTime: new Date() }), - ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] - ] - ]; - } +const SayNow = ({ dateTime }) => { + return ['h1', {}, [`It is: ${dateTime}`]]; +}; + +const App = () => { + return ['div', { 'className': 'header' }, + [SayNow({ dateTime: new Date() }), + ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] + ] + ]; +} - render(createElement(App()), document.getElementById('root')); +render(createElement(App()), document.getElementById('root')); #+END_SRC We are creating two components, that output JSM, as we defined it @@ -745,8 +748,8 @@ The next step is to call render multiple times. #+BEGIN_SRC javascript setInterval(() => - render(createElement(App()), document.getElementById('root')), - 1000); + render(createElement(App()), document.getElementById('root')), + 1000); #+END_SRC If you run the code above you will see the DateTime display being @@ -787,7 +790,7 @@ cause of a performance bottleneck? Or how do you use the React APIs in a performant way? These types of questions should be easier to track down and understand with the foundations covered and I hope this is only the start of your High-Performance React journey. -* Image Test +* Image Test :noexport: :PROPERTIES: :EXPORT_FILE_NAME: manuscript/image-test.markua :END: diff --git a/manuscript/Book.txt b/manuscript/Book.txt index 391bbcc..ee146c6 100644 --- a/manuscript/Book.txt +++ b/manuscript/Book.txt @@ -10,4 +10,3 @@ reconciliation.markua fibers.markua putting-it-all-together.markua conclusion.markua -image-test.markua diff --git a/manuscript/markup-in-javascript---jsx-.markua b/manuscript/markup-in-javascript---jsx-.markua index 575e25c..d8ddc1d 100644 --- a/manuscript/markup-in-javascript---jsx-.markua +++ b/manuscript/markup-in-javascript---jsx-.markua @@ -34,7 +34,8 @@ This is what I'm thinking: ``` ['div', { 'className': 'header' }, [['h1', {}, ['Hello']], - ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] + ['input', { 'type': 'submit', 'disabled': 'disabled' }, + []] ] ] ``` diff --git a/manuscript/putting-it-all-together.markua b/manuscript/putting-it-all-together.markua index 158e43b..10951e3 100644 --- a/manuscript/putting-it-all-together.markua +++ b/manuscript/putting-it-all-together.markua @@ -5,15 +5,15 @@ At this point the only thing left to do is to create some components and use the {format: "javascript"} ``` const SayNow = ({ dateTime }) => { - return ['h1', {}, [`It is: ${dateTime}`]]; + return ['h1', {}, [`It is: ${dateTime}`]]; }; const App = () => { - return ['div', { 'className': 'header' }, - [SayNow({ dateTime: new Date() }), - ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] - ] - ]; + return ['div', { 'className': 'header' }, + [SayNow({ dateTime: new Date() }), + ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] + ] + ]; } render(createElement(App()), document.getElementById('root')); @@ -26,8 +26,8 @@ The next step is to call render multiple times. {format: "javascript"} ``` setInterval(() => - render(createElement(App()), document.getElementById('root')), - 1000); + render(createElement(App()), document.getElementById('root')), + 1000); ``` If you run the code above you will see the DateTime display being updated every second. And if you watch in your dev tools or if you profile the run you will see that the only part of the DOM that gets updated or replaced is the part that changes (aside from the DOM props). We now have a working version of our own React. diff --git a/manuscript/reconciliation.markua b/manuscript/reconciliation.markua index 6591c4f..0a7c8d3 100644 --- a/manuscript/reconciliation.markua +++ b/manuscript/reconciliation.markua @@ -26,13 +26,13 @@ While keeping that in mind, here is an overview of the algorithm we will be impl {format: "javascript"} ``` if (!element && prevElement) - // delete dom element + // delete dom element else if (element && !prevElement) - // add new dom element, render children + // add new dom element, render children else if (element.type === prevElement.type) - // update dom element, render children + // update dom element, render children else if (element.type !== prevElement.type) - // replace dom element, render children + // replace dom element, render children ``` 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. @@ -45,10 +45,10 @@ Here we begin by adding a global object that will store our last render tree, ke ``` const renderTrees = {}; function render(element, container) { - const tree = - render_internal(element, container, renderTrees[container]); - // render complete, store the updated tree - renderTrees[container] = tree; + const tree = + render_internal(element, container, renderTrees[container]); + // render complete, store the updated tree + renderTrees[container] = tree; } ``` @@ -59,34 +59,34 @@ Now that we have stored our last render tree we can go ahead and update our rend {format: "javascript"} ``` function render_internal(element, container, prevElement) { - let domElement, children; - if (!element && prevElement) { - removeDOMElement(prevElement); - return; - } else if (element && !prevElement) { - domElement = createDOMElement(element); - } else if (element.type === prevElement.type) { - domElement = prevElement.domElement; - } else { // types don't match - removeDOMElement(prevElement); - domElement = createDOMElement(element); - } - setDOMProps(element, domElement, prevElement); - children = renderChildren(element, domElement, prevElement); - - if (!prevElement || domElement !== prevElement.domElement) { - container.appendChild(domElement); - } - - return { - domElement: domElement, - parent: container, - type: element.type, - props: { - ...element.props, - children: children - } - }; + let domElement, children; + if (!element && prevElement) { + removeDOMElement(prevElement); + return; + } else if (element && !prevElement) { + domElement = createDOMElement(element); + } else if (element.type === prevElement.type) { + domElement = prevElement.domElement; + } else { // types don't match + removeDOMElement(prevElement); + domElement = createDOMElement(element); + } + setDOMProps(element, domElement, prevElement); + children = renderChildren(element, domElement, prevElement); + + if (!prevElement || domElement !== prevElement.domElement) { + container.appendChild(domElement); + } + + return { + domElement: domElement, + parent: container, + type: element.type, + props: { + ...element.props, + children: children + } + }; } ``` @@ -101,7 +101,7 @@ Removing a DOM element is straightforward; we just `removeChild` on the parent e {format: "javascript"} ``` function removeDOMElement(prevElement) { - prevElement.parent.removeChild(prevElement.domElement); + prevElement.parent.removeChild(prevElement.domElement); } ``` @@ -110,9 +110,9 @@ In creating a new DOM element we just need to branch if we are creating a text e {format: "javascript"} ``` function createDOMElement(element) { - return element.type === 'TEXT' ? - document.createTextNode(element.props.nodeValue) : - document.createElement(element.type); + return element.type === 'TEXT' ? + document.createTextNode(element.props.nodeValue) : + document.createElement(element.type); } ``` @@ -121,18 +121,18 @@ To set the props on an element, we first clear all the existing props and then l {format: "javascript"} ``` function setDOMProps(element, domElement, prevElement) { - if (prevElement) { - Object.keys(prevElement.props) - .filter((key) => key !== 'children') - .forEach((key) => { - domElement[key] = ''; // clear prop - }); - } - Object.keys(element.props) - .filter((key) => key !== 'children') - .forEach((key) => { - domElement[key] = element.props[key]; - }); + if (prevElement) { + Object.keys(prevElement.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = ''; // clear prop + }); + } + Object.keys(element.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = element.props[key]; + }); } ``` @@ -145,17 +145,17 @@ For rendering children we use two loops. The first loop removes any elements tha {format: "javascript"} ``` function renderChildren(element, domElement, prevElement = { props: { children: [] }}) { - const elementLen = element.props.children.length; - const prevElementLen = prevElement.props.children.length; - // remove now unused elements - for (let i = elementLen; i < prevElementLen - elementLen; i++) { - removeDOMElement(element.props.children[i]); - } - // render existing and new elements - return element.props.children.map((child, i) => { - const prevChild = i < prevElementLen ? prevElement.props.children[i] : undefined; - return render_internal(child, domElement, prevChild); - }); + const elementLen = element.props.children.length; + const prevElementLen = prevElement.props.children.length; + // remove now unused elements + for (let i = elementLen; i < prevElementLen - elementLen; i++) { + removeDOMElement(element.props.children[i]); + } + // render existing and new elements + return element.props.children.map((child, i) => { + const prevChild = i < prevElementLen ? prevElement.props.children[i] : undefined; + return render_internal(child, domElement, prevChild); + }); } ``` diff --git a/manuscript/render.markua b/manuscript/render.markua index a9d6bb7..be3f8a4 100644 --- a/manuscript/render.markua +++ b/manuscript/render.markua @@ -14,10 +14,10 @@ This is the same signature as that of React itself. We begin by just focusing on {format: "javascript"} ``` function render(element, container) { - const domElement = createDOMElement(element); - setProps(element, domElement); - renderChildren(element, domElement); - container.appendChild(domElement); + const domElement = createDOMElement(element); + setProps(element, domElement); + renderChildren(element, domElement); + container.appendChild(domElement); ``` Our DOM element is created first. Then we set the properties, render children elements, and finally append the whole tree to the container. @@ -29,23 +29,24 @@ Now that we have an idea of what to build we will work on expanding the pseudoco {format: "javascript"} ``` function render(element, container) { - const { type, props } = element; + const { type, props } = element; - // create the DOM element - const domElement = type === 'TEXT' ? - document.createTextNode(props.nodeValue) : - document.createElement(type); + // create the DOM element + const domElement = type === 'TEXT' ? + document.createTextNode(props.nodeValue) : + document.createElement(type); - // set its properties - Object.keys(props) - .filter((key) => key !== 'children') - .forEach((key) => domElement[key] = props[key]); + // 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)); + // render its children + props.children.forEach((child) => + render(child, domElement)); - // add our tree to the DOM! - container.appendChild(domElement); + // add our tree to the DOM! + container.appendChild(domElement); } ```