Fully working React re-creation.

This commit is contained in:
2020-07-25 14:35:30 -07:00
parent f3a483fe02
commit 66faaa4efe
2 changed files with 250 additions and 78 deletions

128
code/hp-react.js Normal file
View File

@@ -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);
});
}

View File

@@ -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.
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.
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
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,60 +453,93 @@ function render(element, 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.
ahead and update our render method with the heuristics.
TODO note that we are adding parent and domElement properties.
#+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;
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 {
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));
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,
...element
type: element.type,
props: {
...element.props,
children: children
}
};
}
#+END_SRC
#+BEGIN_SRC javascript
function removeDOMElement(prevElement) {
prevElement.parent.removeChild(prevElement.domElement);
}
#+END_SRC
#+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 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];
});
}
#+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
TODO don't figure event handlers are handled specially
** Commit Phase