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:
|
This is what I'm thinking:
|
||||||
|
|
||||||
#+BEGIN_SRC javascript
|
#+BEGIN_SRC javascript
|
||||||
['div', { 'class': 'header' },
|
['div', { 'className': 'header' },
|
||||||
[['h1', {}, ['Hello']],
|
[['h1', {}, ['Hello']],
|
||||||
['input', { 'type': 'submit', 'disabled': 'disabled' }, []]
|
['input', { 'type': 'submit', 'disabled': 'disabled' }, []]
|
||||||
]
|
]
|
||||||
@@ -321,43 +321,16 @@ function render(element, container) {
|
|||||||
container.appendChild(domElement);
|
container.appendChild(domElement);
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
Because the browser APIs for text elements are different than for generic
|
TODO and now full code:
|
||||||
DOM elements and because text elements can't have children we will
|
|
||||||
split up the process in to two methods: ~renderTextElement~ and
|
|
||||||
~renderDOMElement~.
|
|
||||||
|
|
||||||
#+BEGIN_SRC javascript
|
#+BEGIN_SRC javascript
|
||||||
function render(element, container) {
|
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;
|
const { type, props } = element;
|
||||||
|
|
||||||
// create the DOM element
|
// create the DOM element
|
||||||
const domElement = document.createElement(type);
|
const domElement = type === 'TEXT' ?
|
||||||
|
document.createTextNode(props.nodeValue) :
|
||||||
|
document.createElement(type);
|
||||||
|
|
||||||
// set its properties
|
// set its properties
|
||||||
Object.keys(props)
|
Object.keys(props)
|
||||||
@@ -374,6 +347,9 @@ function renderDOMElement(element, container) {
|
|||||||
}
|
}
|
||||||
#+END_SRC
|
#+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
|
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~
|
properties. To do this we first need to filter out the ~children~
|
||||||
property and then we simply loop over they keys setting each property
|
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
|
its details have leaked out in some ways and are overall unlikely to
|
||||||
change in major ways without larger changes to React.
|
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 some kind of call-out for big deal
|
||||||
|
|
||||||
TODO https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
|
TODO https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
|
||||||
|
|
||||||
The approach we will take here is to integrate the heuristics that
|
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
|
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
|
Before we get into the code changes that implement the heuristics it
|
||||||
methods. First, we need to be able to store and retrieve the previous
|
is important to remember that React /only/ looks at an element's type,
|
||||||
render tree. Then we need to add code to compare parts of the tree to
|
existence, and key. It does not do any other diffing. It does not diff
|
||||||
decide if we need to re-render something or if we can re-use it from
|
props. It does not diff sub-trees of modified parents. If you could
|
||||||
the previous render tree.
|
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
|
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
|
#+BEGIN_SRC javascript
|
||||||
const renderTrees = {};
|
const renderTrees = {};
|
||||||
@@ -442,60 +453,93 @@ function render(element, container) {
|
|||||||
// render complete, store the updated tree
|
// render complete, store the updated tree
|
||||||
renderTrees[container] = 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
|
#+END_SRC
|
||||||
|
|
||||||
Now that we have a way to see what we rendered last time we can go
|
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
|
#+BEGIN_SRC javascript
|
||||||
function renderTextElement(element, container, prevElement) {
|
function render_internal(element, container, prevElement) {
|
||||||
const { nodeValue } = element.props;
|
let domElement, children;
|
||||||
let domElement;
|
if (!element && prevElement) {
|
||||||
if (element && prevElement &&
|
removeDOMElement(prevElement);
|
||||||
element.type === prevElement.type &&
|
return;
|
||||||
prevElement.props.nodeValue &&
|
} else if (element && !prevElement) {
|
||||||
nodeValue !== prevElement.props.nodeValue) {
|
domElement = createDOMElement(element);
|
||||||
// types match but values don't; update
|
} else if (element.type === prevElement.type) {
|
||||||
prevElement.domElement.nodeValue = nodeValue;
|
|
||||||
domElement = prevElement.domElement;
|
domElement = prevElement.domElement;
|
||||||
} else {
|
} else {
|
||||||
if (element && prevElement &&
|
removeDOMElement(prevElement);
|
||||||
element.type !== prevElement.type) {
|
domElement = createDOMElement(element);
|
||||||
// element types don't match so remove & append
|
|
||||||
prevElement.parent.removeChild(prevElement.domElement);
|
|
||||||
} else if () {
|
|
||||||
// TODO delete node
|
|
||||||
}
|
}
|
||||||
// new type or new text node
|
setDOMProps(element, domElement, prevElement);
|
||||||
domElement =
|
children = renderChildren(element, domElement, prevElement);
|
||||||
container.appendChild(document.createTextNode(nodeValue));
|
|
||||||
|
if (!prevElement || domElement !== prevElement.domElement) {
|
||||||
|
container.appendChild(domElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domElement: domElement,
|
domElement: domElement,
|
||||||
parent: container,
|
parent: container,
|
||||||
...element
|
type: element.type,
|
||||||
|
props: {
|
||||||
|
...element.props,
|
||||||
|
children: children
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#+END_SRC
|
#+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
|
TODO don't figure event handlers are handled specially
|
||||||
|
|
||||||
** Commit Phase
|
** Commit Phase
|
||||||
|
|||||||
Reference in New Issue
Block a user