src/lib/React3ComponentTree.js
// see ReactDOMComponentTree
import invariant from 'fbjs/lib/invariant';
import Flags from './React3ComponentFlags';
import ID_PROPERTY_NAME from './utils/idPropertyName';
const internalInstanceKey = `__react3InternalInstance$${Math.random().toString(36).slice(2)}`;
/**
* Drill down (through composites and empty components) until we get a host or
* host text component.
*
* This is pretty polymorphic but unavoidable with the current structure we have
* for `_renderedChildren`.
*/
function getRenderedHostOrTextFromComponent(component) {
let result = component;
let rendered = result._renderedComponent;
while (rendered) {
result = rendered;
rendered = result._renderedComponent;
}
return result;
}
/**
* Populate `_hostMarkup` on the rendered host/text component with the given
* markup. The passed `instance` can be a composite.
*/
function precacheMarkup(instance, markup) {
invariant(!!markup, 'Markup is null!');
const hostInstance = getRenderedHostOrTextFromComponent(instance);
hostInstance._hostMarkup = markup;
markup[internalInstanceKey] = hostInstance;
}
function uncacheMarkup(inst) {
const markup = inst._hostMarkup;
if (markup) {
delete markup[internalInstanceKey];
inst._hostMarkup = null;
}
}
/**
* Populate `_hostMarkup` on each child of `inst`, assuming that the children
* match up with the children of `markup`.
*
* We cache entire levels at once to avoid an n^2 problem where we access the
* children of a markup sequentially and have to walk from the start to our target
* markup every time.
*
* Since we update `_renderedChildren` and the actual DOM at (slightly)
* different times, we could race here and see a newer `_renderedChildren` than
* the markups we see. To avoid this, ReactMultiChild calls
* `prepareToManageChildren` before we change `_renderedChildren`, at which
* time the container's child markups are always cached (until it unmounts).
*/
function precacheChildMarkups(instance, markup) {
if ((instance._flags & Flags.hasCachedChildMarkups) !== 0) {
return;
}
const renderedChildren = instance._renderedChildren;
const childrenNames = Object.keys(renderedChildren);
const childrenMarkup = markup.childrenMarkup;
/* eslint-disable no-labels, no-unused-labels, no-restricted-syntax */
outer: for (let i = 0; i < childrenNames.length; ++i) {
/* eslint-enable no-labels, no-unused-labels, no-restricted-syntax */
const childName = childrenNames[i];
const childInst = renderedChildren[childName];
// TODO implement _domID
const childID = getRenderedHostOrTextFromComponent(childInst)._hostID;
if (childID === 0) {
// We're currently unmounting this child in ReactMultiChild; skip it.
continue;
}
for (let j = 0; j < childrenMarkup.length; ++j) {
const childMarkup = childrenMarkup[j];
if (childMarkup[ID_PROPERTY_NAME] === childID) {
precacheMarkup(childInst, childMarkup);
continue outer; // eslint-disable-line no-labels
}
}
// We reached the end of the DOM children without finding an ID match.
if (process.env.NODE_ENV !== 'production') {
invariant(false, 'Unable to find element with ID %s.', childID);
} else {
invariant(false);
}
/* original implementation:
// We assume the child nodes are in the same order as the child instances.
for (; childMarkup !== null; childMarkup = childMarkup.nextSibling) {
if (childMarkup.nodeType === 1 && // Element.ELEMENT_NODE
childMarkup.getAttribute(ATTR_NAME) === String(childID) ||
childMarkup.nodeType === 8 &&
childMarkup.nodeValue === ` react-text: ${childID} ` ||
childMarkup.nodeType === 8 &&
childMarkup.nodeValue === ` react-empty: ${childID} `) {
precacheNode(childInst, childMarkup);
continue outer; // eslint-disable-line no-labels
}
}
*/
}
instance._flags |= Flags.hasCachedChildMarkups;
}
// see ReactDOMComponentTree:getClosestInstanceFromNode
function getClosestInstanceFromMarkup(markup) {
if (markup[internalInstanceKey]) {
return markup[internalInstanceKey];
}
let currentMarkup = markup;
// Walk up the tree until we find an ancestor whose instance we have cached.
const parentMarkupsWithoutInstanceKey = [];
while (!currentMarkup[internalInstanceKey]) {
parentMarkupsWithoutInstanceKey.push(currentMarkup);
if (currentMarkup.parentMarkup) {
currentMarkup = currentMarkup.parentMarkup;
} else {
// Top of the tree. This markup must not be part of a React tree (or is
// unmounted, potentially).
return null;
}
}
// if we're here, then currentMarkup does have internalInstanceKey, otherwise
// we would have reached the top of the tree and returned null.
let closest;
let instance = currentMarkup[internalInstanceKey];
// traversing from greatest ancestor (e.g. parent of all parents) downwards
// e.g. walk down the tree now
while (instance) {
closest = instance;
if (!parentMarkupsWithoutInstanceKey.length) {
break;
}
// this will ensure that all children of the current greatest ancestor
// have internalInstanceKey
precacheChildMarkups(instance, currentMarkup);
currentMarkup = parentMarkupsWithoutInstanceKey.pop();
instance = currentMarkup[internalInstanceKey];
}
/* original impl of ^
for (; currentMarkup && (instance = currentMarkup[internalInstanceKey]);
currentMarkup = parentMarkupsWithoutInstanceKey.pop()) {
closest = instance;
if (parentMarkupsWithoutInstanceKey.length) {
this.precacheChildMarkups(instance, currentMarkup);
}
}
*/
return closest;
}
// see ReactDOMComponentTree:getInstanceFromNode
function getInstanceFromMarkup(markup) {
const inst = getClosestInstanceFromMarkup(markup);
if (inst !== null && inst._hostMarkup === markup) {
return inst;
}
return null;
}
/**
* Given an InternalComponent, return the corresponding
* host markup.
*/
function getMarkupFromInstance(inst) {
// Without this first invariant, passing a non-React3-component triggers the next
// invariant for a missing parent, which is super confusing.
if (process.env.NODE_ENV !== 'production') {
invariant(
inst._hostMarkup !== undefined,
'getMarkupFromInstance: Invalid argument.'
);
} else {
invariant(
inst._hostMarkup !== undefined
);
}
if (inst._hostMarkup) {
return inst._hostMarkup;
}
let currentInstance = inst;
// Walk up the tree until we find an ancestor whose host node we have cached.
const parents = [];
while (!currentInstance._hostMarkup) {
parents.push(currentInstance);
invariant(
currentInstance._hostParent,
'React3 tree root should always have a node reference.'
);
currentInstance = currentInstance._hostParent;
}
// Now parents contains each ancestor that does *not* have a cached host
// markup, and `currentInstance` is the deepest ancestor that does.
for (; parents.length; currentInstance = parents.pop()) {
precacheChildMarkups(currentInstance, currentInstance._hostMarkup);
}
return currentInstance._hostMarkup;
}
module.exports = {
getMarkupFromInstance,
getInstanceFromMarkup,
precacheMarkup,
uncacheMarkup,
precacheChildMarkups,
getClosestInstanceFromMarkup,
getRenderedHostOrTextFromComponent,
};