Skip to content

Instantly share code, notes, and snippets.

@kaustubh-karkare
Last active August 23, 2019 17:23
Show Gist options
  • Save kaustubh-karkare/37c27c7ce7840e4a773962ad6c9ecd9c to your computer and use it in GitHub Desktop.
Save kaustubh-karkare/37c27c7ce7840e4a773962ad6c9ecd9c to your computer and use it in GitHub Desktop.
const React = {};
/**
* ReactElements are lightweight objects which contain information necessary to
* create the equivalent DOM nodes. All JSX tags are transformed into functions
* that return instances of this class.
* Note that the decision to actual create those nodes and insert them into the
* document hasn't been made yet: it is possible that that might never happen.
*/
class ReactElement {
constructor(type, props) {
this.type = type; // see ReactComponent.create
this.key = props.key || null;
delete props.key;
props.children = this._flattenChildren(props.children);
this.props = props;
}
_flattenChildren(item) {
if (item == null) {
return [];
} else if (item instanceof ReactElement) {
return [item];
} else if (Array.isArray(item)) {
const result = [];
for (let ii = 0; ii < item.length; ++ii) {
result.push(...this._flattenChildren(item[ii]));
}
return result;
} else {
return [new ReactElement(null, {innerText: item.toString()})];
}
}
}
/**
* This is used by ReactComponents to collect callbacks which are to be
* executed after the entire operation (mount/update/unmount) is complete.
*/
class ReactTransaction {
constructor() {
this._queue = [];
}
onComplete(...callbacks) {
this._queue.push(...callbacks);
}
complete() {
this._queue.map(callback => callback());
}
}
/**
* While the real DOM is constructed using the built-in HTMLElement class
* instances, React's Virtual DOM is built using ReactComponent instances.
* A ReactComponent is created using the data contained in the corresponding
* ReactElement, and exposes an API that returns the equivalent HTMLElement
* that should be inserted into the DOM.
*/
class ReactComponent {
constructor(element) {
this.element = element;
invariant(
typeof this.mount === 'function' &&
typeof this.getNode === 'function' &&
typeof this.update === 'function' &&
typeof this.unmount === 'function',
'incomplete implementation',
);
}
static create(element) {
if (element.type === null) {
return new ReactTextComponent(element);
} else if (typeof element.type === 'string') {
return new ReactNativeComponent(element);
} else if (typeof element.type === 'function') {
return new ReactCompositeComponent(element);
} else {
invariant(false, 'invalid element');
}
}
/**
* Reconciliation is the process of performing the smallest number of
* operations necessary to update the DOM into the desired state,
* since these operations are the performance bottleneck.
* The main advantage of the Virtual DOM is that all the information necessary
* to determine those minimum changes is already available, avoiding the need
* to look into the real DOM (which would, again, be slower).
* The new ReactElement represents how the corresponding subtree should look
* like after this process, and updating an existing component is preferred
* over rebuilding everything from scratch.
* It is the responsibility of the caller to check if the component being
* returned is the same one, and if not, make appropriate changes
* (including unmounting the original, and mounting the replacement).
*/
static reconcile(transaction, component, nextElement) {
const prevElement = component.element;
if (prevElement.type === nextElement.type) {
// Given that the elements are of the same type, it is reasonable to
// assume that updating the existing subtree is faster.
component.update(transaction, nextElement);
return component;
} else {
// Since they are different types of elements, updating is not possible,
// and so we have no choice but to remove and rebuild the subtree.
return ReactComponent.create(transaction, nextElement);
}
}
}
class ReactTextComponent extends ReactComponent {
constructor(element) {
super(element);
this._node = null;
}
mount(transaction) {
this._node = document.createElement('span');
this._node.innerText = this.element.props.innerText;
return this._node;
}
getNode() {
return this._node;
}
update(transaction, newElement) {
const oldElement = this.element;
if (newElement.props.innerText !== oldElement.props.innerText) {
this._node.innerText = newElement.props.innerText;
}
this.element = newElement;
return this._node;
}
unmount(transaction) {
const node = this._node;
this._node = null
return node;
}
}
/**
* Each ReactNativeComponent instance in the VDOM
* corresponds to an HTMLElement instance in the real DOM.
*/
class ReactNativeComponent extends ReactComponent {
constructor(element) {
super(element);
this._node = null;
this._childComponents = null;
}
mount(transaction) {
const element = this.element;
this._node = document.createElement(element.type);
for (const name in element.props) {
if (name === 'children') continue;
const value = element.props[name];
if (typeof value === 'function') {
this._node.addEventListener(name, value);
} else {
this._node.setAttribute(name, value);
}
}
this._childComponents = [];
for (let ii = 0; ii < element.props.children.length; ++ii) {
const childElement = element.props.children[ii];
const childComponent = ReactComponent.create(childElement);
const childNode = childComponent.mount();
this._node.appendChild(childNode);
this._childComponents.push(childComponent);
}
return this._node;
}
getNode() {
return this._node;
}
update(transaction, newElement) {
const oldElement = this.element;
// 1. Fix attributes and event listeners.
// 1.1. Remove all props that are not in the newElement.
for (const name in oldElement.props) {
if (name === 'children') continue;
const oldValue = oldElement.props[name];
if (!(name in newElement.props)) {
if (typeof oldValue === 'function') {
this._node.removeEventListener(name, oldValue);
} else {
this._node.removeAttribute(name);
}
}
}
// 1.2. Set/update all props that are in the newElement.
for (const name in newElement.props) {
if (name === 'children') continue;
const newValue = newElement.props[name];
const oldValue = oldElement.props[name];
if (newValue !== oldValue) {
if (typeof value === 'function') {
this._node.removeEventListener(name, oldValue);
this._node.addEventListener(name, newValue);
} else {
this._node.setAttribute(name, newValue);
}
}
}
// 2. Rearrange elements/components/nodes based on keys to minimize operations.
// 2.1. Create a map of the locations of existing children with keys.
const keyToIndex = {};
for (let ii = 0; ii < oldElement.props.children.length; ++ii) {
const oldChildKey = oldElement.props.children[ii].key;
if (oldChildKey) {
invariant(!keyToIndex.hasOwnProperty(oldChildKey), 'duplicate keys');
keyToIndex[oldChildKey] = ii;
}
}
// 2.2. Iterate through the new children with keys, and if it turns out that
// the old child with the same key is not at the current location, exchange
// whatever was at the current location with wherever the old child is.
for (let ii = 0; ii < newElement.props.children.length; ++ii) {
const newChildKey = newElement.props.children[ii].key;
if (
newChildKey &&
keyToIndex.hasOwnProperty(newChildKey) &&
keyToIndex[newChildKey] !== ii
) {
const jj = keyToIndex[newChildKey];
// 2.2.1. Exchange locations in the map.
keyToIndex[newChildKey] = ii;
const oldChildKey = oldElement.props.children[ii].key;
if (oldChildKey) keyToIndex[oldChildKey] = jj;
// 2.2.2. Exchange elements & components.
[
oldElement.props.children[ii],
oldElement.props.children[jj],
this._childComponents[ii],
this._childComponents[jj],
] = [
oldElement.props.children[jj],
oldElement.props.children[ii],
this._childComponents[jj],
this._childComponents[ii],
];
// 2.2.3. Exchange nodes.
const newChildNode = this._node.childNodes[ii];
const oldChildNode = this._node.childNodes[jj];
const newChildSibling = newChildNode.nextSibling;
const oldChildSibling = oldChildNode.nextSibling;
if (newChildNode === oldChildSibling) {
this._node.removeChild(newChildNode);
this._node.insertBefore(newChildNode, oldChildNode);
} else if (oldChildNode === newChildSibling) {
this._node.removeChild(oldChildNode);
this._node.insertBefore(oldChildNode, newChildNode);
} else {
this._node.removeChild(newChildNode);
if (oldChildSibling) {
this._node.insertBefore(newChildNode, oldChildSibling);
} else {
this._node.appendChild(newChildNode);
}
this._node.removeChild(oldChildNode);
if (newChildSibling) {
this._node.insertBefore(oldChildNode, newChildSibling);
} else {
this._node.appendChild(oldChildNode);
}
}
}
}
// 3. Recursively reconcile all child components.
for (let ii = 0; ii < newElement.props.children.length; ++ii) {
const newChildElement = newElement.props.children[ii];
if (ii < this._childComponents.length) {
// 3.1. Try and update existing components, but replace them if not possible.
const oldChildComponent = this._childComponents[ii];
const newChildComponent = ReactComponent.reconcile(
transaction,
oldChildComponent,
newChildElement,
);
if (newChildComponent !== oldChildComponent) {
this._childComponents[ii] = newChildComponent;
this._node.replaceChild(
newChildComponent.mount(),
oldChildComponent.unmount(),
);
}
} else {
// 3.2. Create new components if needed.
const newChildComponent = ReactComponent.create(newChildElement);
const newChildNode = newChildComponent.mount();
this._node.appendChild(newChildNode);
this._childComponents.push(newChildComponent);
}
}
// 3.3. Remove excess components no longer needed.
while (this._childComponents.length > newElement.props.children.length) {
this._node.removeChild(this._childComponents.pop().unmount());
}
this.element = newElement;
return this._node;
}
unmount(transaction) {
this._childComponents.forEach(childComponent => childComponent.unmount());
this._childComponents = null;
const node = this._node;
this._node = null;
return node;
}
}
/**
* A ReactCompositeComponent goes invokes the following methods
* at the various stages of its lifecycle:
*
* (unmounted)
* getInitialState()
* componentWillMount()
* render()
* (mounted)
* componentDidMount()
* (waiting for something to happen)
*
* componentWillReceiveProps()
* shouldComponentUpdate()
* componentWillUpdate()
* render()
* (still mounted, now updated)
* componentDidUpdate()
*
* (waiting for something to happen)
* componentWillUnmount()
* (status = unmounted)
*
* Note that all ReactCompositeComponents will ultimately be
* decomposed into ReactNativeComponents while rendering.
*/
class ReactCompositeComponent extends ReactComponent {
constructor(element) {
super(element);
this._childComponent = null;
// for creation
const instance = new element.type();
for (const name in instance) {
if (typeof instance[name] === 'function') {
instance[name] = instance[name].bind(instance);
}
}
instance.props = element.props;
if (instance.getInitialState) {
instance.state = instance.getInitialState();
}
this._instance = instance;
// for updating
this._updateQueue = [];
this._updateCallbacks = [];
this._updateTimeout = null;
instance.setState = function(update, callback) {
this._updateQueue.push(update);
if (callback) this._updateCallbacks.push(callback);
this._scheduleUpdate();
}.bind(this);
instance.forceUpdate = function(callback) {
if (callback) this._updateCallbacks.push(callback);
this._scheduleUpdate();
}.bind(this);
}
mount(transaction) {
const instance = this._instance;
if (instance.componentWillMount) {
instance.componentWillMount();
}
if (instance.componentDidMount) {
transaction.onComplete(instance.componentDidMount);
}
const element = instance.render();
this._childComponent = ReactComponent.create(element);
this._childComponent.mount();
return this.getNode();
}
getNode() {
return this._childComponent.getNode();
}
_scheduleUpdate() {
if (!this._updateTimeout) {
this._updateTimeout = window.setTimeout(function() {
this._updateTimeout = null;
const transaction = new ReactTransaction();
const oldNode = this.getNode();
const newNode = this.update(transaction, null);
if (oldNode !== newNode) {
oldNode.parentNode.replaceChild(oldNode, newNode);
}
transaction.complete();
}.bind(this), 0);
}
}
update(transaction, newElement) {
const instance = this._instance;
let nextProps = instance.props;
if (newElement != null) {
// This method was not called via setState/forceUpdate.
nextProps = newElement.props;
if (instance.componentWillReceiveProps) {
instance.componentWillReceiveProps(nextProps);
}
}
let nextState = instance.state;
this._updateQueue.forEach(function(update) {
if (typeof update === 'function') {
nextState = {...nextState, ...update(nextState)};
} else {
nextState = {...nextState, ...update};
}
});
const shouldUpdate = instance.shouldComponentUpdate
? instance.shouldComponentUpdate(nextProps, nextState)
: true;
if (shouldUpdate) {
if (instance.componentWillUpdate) {
instance.componentWillUpdate(nextProps, nextState);
}
if (instance.componentDidUpdate) {
const prevProps = instance.props;
const prevState = instance.state;
transaction.onComplete(
instance.componentDidUpdate.bind(instance, nextProps, nextState),
);
}
}
instance.props = nextProps;
instance.state = nextState;
if (shouldUpdate) {
const childComponent = ReactComponent.reconcile(
transaction,
this._childComponent,
instance.render(),
);
if (this._childComponent !== childComponent) {
this._childComponent.unmount();
childComponent.mount();
this._childComponent = childComponent;
}
}
this._updateQueue = [];
this._updateCallbacks = [];
this.element = newElement;
return this.getNode();
}
unmount(transaction) {
const instance = this._instance;
if (instance.componentWillUnmount) {
instance.componentWillUnmount();
}
const childComponent = this._childComponent;
this._childComponent = null;
return childComponent.unmount();
}
}
React.PropTypes = function() {
const typeValidator = function(expectedType) {
return function(propName, props) {
if (props[propName] == null) return;
let actualType = typeof props[propName];
if (Array.isArray(props[propName])) actualType = 'array';
invariant(actualType === expectedType, 'invalid prop type');
};
};
const possiblyRequired = function(validator) {
validator.isRequired = function(propName, props) {
invariant(props[propName] != null, 'missing prop');
validator(propName, props);
};
return validator;
};
return {
string: possiblyRequired(typeValidator('string')),
number: possiblyRequired(typeValidator('number')),
object: possiblyRequired(typeValidator('object')),
array: possiblyRequired(typeValidator('array')),
func: possiblyRequired(typeValidator('function')),
};
}.call(null);
// create standard html element classes
React.DOM = function(tags) {
const nativeElements = {};
tags.forEach(function(tag) {
nativeElements[tag] = function(props = {}, ...children) {
props.children = children;
return new ReactElement(tag, props);
};
});
return nativeElements;
}.call(null, ['div', 'span', 'ul', 'li', 'a']);
// api for creating composite components
React.createClass = function(spec) {
const Constructor = function() {};
let defaultProps = {};
if (spec.getDefaultProps) {
defaultProps = spec.getDefaultProps();
delete spec.getDefaultProps;
}
let propTypes = {};
if (spec.propTypes) {
propTypes = spec.propTypes;
delete spec.propTypes;
}
for (const key in spec) {
Constructor.prototype[key] = spec[key];
}
return function(props = {}, ...children) {
for (const name in defaultProps) {
if (!(name in props)) {
props[name] = defaultProps[name];
}
}
for (const name in propTypes) {
propTypes[name](name, props);
}
props.children = children;
return new ReactElement(Constructor, props);
};
};
React.mount = function(element, container, callback) {
invariant(container, 'missing container');
invariant(container instanceof HTMLElement, 'invalid container');
while (container.lastChild) container.removeChild(container.lastChild);
let component = ReactComponent.create(element);
const transaction = new ReactTransaction();
if (callback) transaction.onComplete(callback);
container.appendChild(component.mount(transaction));
transaction.complete();
return { // api
update: function(element, callback) {
const transaction = new ReactTransaction();
if (callback) transaction.onComplete(callback);
const newComponent = ReactComponent.reconcile(
transaction,
component,
element,
);
if (newComponent !== component) {
container.replaceChild(newComponent.mount(), component.unmount());
component = newComponent;
}
transaction.complete();
},
unmount: function(callback) {
const transaction = new ReactTransaction();
if (callback) transaction.onComplete(callback);
container.removeChild(component.unmount());
transaction.complete();
},
};
};
function invariant(expression, message) {
if (!expression) {
throw new Error('InvariantViolation: ' + message);
}
}
// Testing
const Counter = React.createClass({
getDefaultProps() {
return {list: [1, 2, 3]};
},
getInitialState() {
return {list: this.props.list};
},
render() {
/**
* Original code:
* <div id="wrapper">
* <a click={this.addItemToList}>{'Click here to add item to list!'}</a>
* <ul>this.state.list.map(item => <li>{item}</li>)</ul>
* </div>
* Assuming that the following is the result of the JSX transform.
*/
return React.DOM.div(
{id: "wrapper"},
React.DOM.a(
{click: this.addItemToList},
'Click here to add item to list!',
),
React.DOM.ul(
{},
this.state.list.map(item => React.DOM.li({}, item)),
),
);
},
addItemToList() {
const list = this.state.list;
list.push(list.length + 1);
this.setState({list});
},
});
const list = React.mount(Counter(), document.getElementById('main'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment