Fully working React re-creation.

master
Thomas Hintz 4 years ago
parent f3a483fe02
commit 66faaa4efe

@ -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.
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.
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.
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,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
function setDOMProps(element, domElement, prevElement) {
if (prevElement) {
Object.keys(prevElement.props)
.filter((key) => key !== 'children')
.forEach((key) => {
domElement[key] = '';
});
}
// new type or new text node
domElement =
container.appendChild(document.createTextNode(nodeValue));
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]);
}
return {
domElement: domElement,
parent: container,
...element
};
// 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

Loading…
Cancel
Save