From 66faaa4efe312bcf85ed977b8482260dc6aca38c Mon Sep 17 00:00:00 2001 From: Thomas Hintz Date: Sat, 25 Jul 2020 14:35:30 -0700 Subject: [PATCH] Fully working React re-creation. --- code/hp-react.js | 128 ++++++++++++++++++++++++ high-performance-react.org | 198 ++++++++++++++++++++++--------------- 2 files changed, 249 insertions(+), 77 deletions(-) create mode 100644 code/hp-react.js diff --git a/code/hp-react.js b/code/hp-react.js new file mode 100644 index 0000000..90b94cf --- /dev/null +++ b/code/hp-react.js @@ -0,0 +1,128 @@ +var d = +['div', { 'className': 'header' }, + [['h1', {}, ['Hello']], + ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] + ] +]; +d = +['div', { 'className': 'header' }, + [['h1', {}, ['Hello, how have you been?']], + ['input', { 'type': 'submit', 'style': 'color: red;' }, []] + ] +]; + +var e = createElement(d); + +render(e, $0) + + + + + +var d = +['div', { 'className': 'header' }, + [['h1', {}, ['Hello']], + ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] + ] +]; + +function createElement(node) { + // an array: not text, number, or other primitive + if (typeof node === 'object') { + const [ tag, props, children ] = node; + return { + type: tag, + props: { + ...props, + children: children.map(createElement) + } + }; + } + + // primitives like text or number + return { + type: 'TEXT', + props: { + nodeValue: node, + children: [] + } + }; +} + +let 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) { + 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 { + 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 + } + }; +} + +function removeDOMElement(prevElement) { + prevElement.parent.removeChild(prevElement.domElement); +} + +function createDOMElement(element) { + return element.type === 'TEXT' ? + document.createTextNode(element.props.nodeValue) : + document.createElement(element.type); +} + +function setDOMProps(element, domElement, prevElement) { + if (prevElement) { + Object.keys(prevElement.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = ''; + }); + } + Object.keys(element.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = element.props[key]; + }); +} + +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); + }); +} diff --git a/high-performance-react.org b/high-performance-react.org index c23d622..c375d85 100644 --- a/high-performance-react.org +++ b/high-performance-react.org @@ -169,7 +169,7 @@ notation. This is what I'm thinking: #+BEGIN_SRC javascript -['div', { 'class': 'header' }, +['div', { 'className': 'header' }, [['h1', {}, ['Hello']], ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] ] @@ -321,43 +321,16 @@ function render(element, container) { 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~. +TODO and now full code: #+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); + const domElement = type === 'TEXT' ? + document.createTextNode(props.nodeValue) : + document.createElement(type); // set its properties Object.keys(props) @@ -374,6 +347,9 @@ function renderDOMElement(element, container) { } #+END_SRC +Next, we look at renderDOMElement which must also set properties on +the newly created DOM element and render any children. + 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 @@ -417,22 +393,57 @@ 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. +According to the React documentation their diffing algorithm is O(n) +and based on two major components: + + - Elements of differing types will yield different trees + - You can hint at tree changes with the ~key~ prop. + +In this section we will focus on the first part: differing +types. Later on we will discuss and implement the ~key~ prop. + 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. +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. If you could +only take away one thing from this book it would that. + +Here is a more in depth look at the algorithm we will be implementing: + +#+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 +#+END_SRC -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. +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. Here we are adding a global object that will store our last render -tree keyed by the container. +tree, keyed by the ~container~. #+BEGIN_SRC javascript const renderTrees = {}; @@ -442,57 +453,90 @@ function render(element, container) { // render complete, store the updated tree renderTrees[container] = tree; } +#+END_SRC +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. + +TODO note that we are adding parent and domElement properties. + +#+BEGIN_SRC javascript function render_internal(element, container, prevElement) { - if (element.type === 'TEXT') { - return renderTextElement(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 { - return renderDOMElement(element, container, prevElement); + 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 -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 +function removeDOMElement(prevElement) { + prevElement.parent.removeChild(prevElement.domElement); +} #+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 createDOMElement(element) { + return element.type === 'TEXT' ? + document.createTextNode(element.props.nodeValue) : + document.createElement(element.type); +} +#+END_SRC #+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)); +function setDOMProps(element, domElement, prevElement) { + if (prevElement) { + Object.keys(prevElement.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = ''; + }); } - return { - domElement: domElement, - parent: container, - ...element - }; + Object.keys(element.props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = element.props[key]; + }); +} +#+END_SRC + +#+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); + }); } #+END_SRC