Fully working React re-creation.
This commit is contained in:
128
code/hp-react.js
Normal file
128
code/hp-react.js
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user