Last active
August 23, 2019 17:23
-
-
Save kaustubh-karkare/37c27c7ce7840e4a773962ad6c9ecd9c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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