rax
Version:
A universal React-compatible render engine.
467 lines (396 loc) • 15 kB
JavaScript
import Host from './host';
import { detachRef, attachRef, updateRef } from './ref';
import instantiateComponent from './instantiateComponent';
import shouldUpdateComponent from './shouldUpdateComponent';
import getElementKeyName from './getElementKeyName';
import getPrevSiblingNativeNode from './getPrevSiblingNativeNode';
import Instance from './instance';
import BaseComponent from './base';
import toArray from '../toArray';
import { isFunction, isArray, isNull } from '../types';
import assign from '../assign';
import { INSTANCE, INTERNAL, NATIVE_NODE } from '../constant';
const STYLE = 'style';
const CHILDREN = 'children';
const TREE = 'tree';
const EVENT_PREFIX_REGEXP = /^on[A-Z]/;
/**
* Native Component
*/
export default class NativeComponent extends BaseComponent {
__mountComponent(parent, parentInstance, context, nativeNodeMounter) {
this.__initComponent(parent, parentInstance, context);
const currentElement = this.__currentElement;
const props = currentElement.props;
const type = currentElement.type;
const children = props[CHILDREN];
const appendType = props.append || TREE; // Default is tree
// Clone a copy for style diff
this.__prevStyleCopy = assign({}, props[STYLE]);
let instance = {
type,
props,
};
instance[INTERNAL] = this;
this[INSTANCE] = instance;
if (appendType === TREE) {
// Should after process children when mount by tree mode
this.__mountChildren(children, context);
this.__mountNativeNode(nativeNodeMounter);
} else {
// Should before process children when mount by node mode
this.__mountNativeNode(nativeNodeMounter);
this.__mountChildren(children, context);
}
// Ref acttach
if (currentElement && currentElement.ref) {
attachRef(currentElement._owner, currentElement.ref, this);
}
if (process.env.NODE_ENV !== 'production') {
Host.reconciler.mountComponent(this);
}
return instance;
}
__mountChildren(children, context) {
if (children == null) return children;
const nativeNode = this.__getNativeNode();
return this.__mountChildrenImpl(nativeNode, toArray(children), context);
}
__mountChildrenImpl(parent, children, context, nativeNodeMounter) {
let renderedChildren = this.__renderedChildren = {};
const renderedChildrenImage = [];
for (let i = 0, l = children.length; i < l; i++) {
const element = children[i];
const renderedChild = instantiateComponent(element);
const name = getElementKeyName(renderedChildren, element, i);
renderedChildren[name] = renderedChild;
renderedChild.__mountIndex = i;
// Mount children
const mountImage = renderedChild.__mountComponent(
parent,
this[INSTANCE],
context,
nativeNodeMounter
);
renderedChildrenImage.push(mountImage);
}
return renderedChildrenImage;
}
__unmountChildren(shouldNotRemoveChild) {
let renderedChildren = this.__renderedChildren;
if (renderedChildren) {
for (let name in renderedChildren) {
let renderedChild = renderedChildren[name];
renderedChild.unmountComponent(shouldNotRemoveChild);
}
this.__renderedChildren = null;
}
}
unmountComponent(shouldNotRemoveChild) {
if (this[NATIVE_NODE]) {
let ref = this.__currentElement.ref;
if (ref) {
detachRef(this.__currentElement._owner, ref, this);
}
Instance.remove(this[NATIVE_NODE]);
if (!shouldNotRemoveChild) {
Host.driver.removeChild(this[NATIVE_NODE], this._parent);
}
}
this.__unmountChildren(true);
this.__prevStyleCopy = null;
this.__destoryComponent();
}
__updateComponent(prevElement, nextElement, prevContext, nextContext) {
// Replace current element
this.__currentElement = nextElement;
updateRef(prevElement, nextElement, this);
let prevProps = prevElement.props;
let nextProps = nextElement.props;
this.__updateProperties(prevProps, nextProps);
// If the prevElement has no child, mount children directly
if (prevProps[CHILDREN] == null ||
isArray(prevProps[CHILDREN]) && prevProps[CHILDREN].length === 0) {
this.__mountChildren(nextProps[CHILDREN], nextContext);
} else {
this.__updateChildren(nextProps[CHILDREN], nextContext);
}
if (process.env.NODE_ENV !== 'production') {
Host.reconciler.receiveComponent(this);
}
}
__updateProperties(prevProps, nextProps) {
let propKey;
let styleName;
let styleUpdates;
const driver = Host.driver;
const nativeNode = this.__getNativeNode();
for (propKey in prevProps) {
// Continue children and null value prop or nextProps has some propKey that do noting
if (
propKey === CHILDREN ||
prevProps[propKey] == null ||
// Use hasOwnProperty here for avoid propKey name is some with method name in object proptotype
nextProps.hasOwnProperty(propKey)
) {
continue;
}
if (propKey === STYLE) {
// Remove all style
let lastStyle = this.__prevStyleCopy;
for (styleName in lastStyle) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
this.__prevStyleCopy = null;
} else if (EVENT_PREFIX_REGEXP.test(propKey)) {
// Remove event
const eventListener = prevProps[propKey];
if (isFunction(eventListener)) {
driver.removeEventListener(
nativeNode,
propKey.slice(2).toLowerCase(),
eventListener
);
}
} else {
// Remove attribute
driver.removeAttribute(
nativeNode,
propKey,
prevProps[propKey]
);
}
}
for (propKey in nextProps) {
let nextProp = nextProps[propKey];
let prevProp = propKey === STYLE ? this.__prevStyleCopy :
prevProps != null ? prevProps[propKey] : undefined;
// Continue children or prevProp equal nextProp
if (
propKey === CHILDREN ||
prevProp === nextProp ||
nextProp == null && prevProp == null
) {
continue;
}
// Update style
if (propKey === STYLE) {
if (nextProp) {
// Clone property
nextProp = this.__prevStyleCopy = assign({}, nextProp);
} else {
this.__prevStyleCopy = null;
}
if (prevProp != null) {
// Unset styles on `prevProp` but not on `nextProp`.
for (styleName in prevProp) {
if (!nextProp || !nextProp[styleName] && nextProp[styleName] !== 0) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `prevProp`.
for (styleName in nextProp) {
if (prevProp[styleName] !== nextProp[styleName]) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Assign next prop when prev style is null
styleUpdates = nextProp;
}
} else if (EVENT_PREFIX_REGEXP.test(propKey)) {
// Update event binding
let eventName = propKey.slice(2).toLowerCase();
if (isFunction(prevProp)) {
driver.removeEventListener(nativeNode, eventName, prevProp, nextProps);
}
if (isFunction(nextProp)) {
driver.addEventListener(nativeNode, eventName, nextProp, nextProps);
}
} else {
// Update other property
if (nextProp != null) {
driver.setAttribute(
nativeNode,
propKey,
nextProp
);
} else {
driver.removeAttribute(
nativeNode,
propKey,
prevProps[propKey]
);
}
if (process.env.NODE_ENV !== 'production') {
Host.measurer && Host.measurer.recordOperation({
instanceID: this._mountID,
type: 'update attribute',
payload: {
[propKey]: nextProp
}
});
}
}
}
if (styleUpdates) {
if (process.env.NODE_ENV !== 'production') {
Host.measurer && Host.measurer.recordOperation({
instanceID: this._mountID,
type: 'update style',
payload: styleUpdates
});
}
driver.setStyle(nativeNode, styleUpdates);
}
}
__updateChildren(nextChildrenElements, context) {
// prev rendered children
let prevChildren = this.__renderedChildren;
let driver = Host.driver;
if (nextChildrenElements == null && prevChildren == null) {
return;
}
let nextChildren = {};
if (nextChildrenElements != null) {
nextChildrenElements = toArray(nextChildrenElements);
// Update next children elements
for (let index = 0, length = nextChildrenElements.length; index < length; index++) {
let nextElement = nextChildrenElements[index];
let name = getElementKeyName(nextChildren, nextElement, index);
let prevChild = prevChildren && prevChildren[name];
let prevElement = prevChild && prevChild.__currentElement;
let prevContext = prevChild && prevChild._context;
// Try to update between the two of some name that has some element type,
// and move child in next children loop if need
if (prevChild != null && shouldUpdateComponent(prevElement, nextElement)) {
if (prevElement !== nextElement || prevContext !== context) {
// Pass the same context when updating children
prevChild.__updateComponent(prevElement, nextElement, context,
context);
}
nextChildren[name] = prevChild;
} else {
// Unmount the prevChild when some name with nextChild but different element type,
// and move child node in next children loop
if (prevChild) {
prevChild.__unmount = true;
}
// The child must be instantiated before it's mounted.
nextChildren[name] = instantiateComponent(nextElement);
}
}
}
let parent = this.__getNativeNode();
let isFragmentParent = isArray(parent);
let prevFirstChild = null;
let prevFirstNativeNode = null;
let isPrevFirstEmptyFragment = false;
let shouldUnmountPrevFirstChild = false;
let lastPlacedNode = null;
// Directly remove all children from component, if nextChildren is empty (null, [], '').
// `driver.removeChildren` is optional driver protocol.
let shouldRemoveAllChildren = Boolean(
driver.removeChildren
// nextChildElements == null or nextChildElements is empty
&& (isNull(nextChildrenElements) || nextChildrenElements && !nextChildrenElements.length)
// Fragment parent can not remove parentNode's all child nodes directly.
&& !isFragmentParent
);
// Unmount children that are no longer present.
if (prevChildren != null) {
for (let name in prevChildren) {
let prevChild = prevChildren[name];
let shouldUnmount = prevChild.__unmount || !nextChildren[name];
// Store old first child ref for append node ahead and maybe delay remove it
if (!prevFirstChild) {
shouldUnmountPrevFirstChild = shouldUnmount;
prevFirstChild = prevChild;
prevFirstNativeNode = prevFirstChild.__getNativeNode();
if (isArray(prevFirstNativeNode)) {
isPrevFirstEmptyFragment = prevFirstNativeNode.length === 0;
prevFirstNativeNode = prevFirstNativeNode[0];
}
} else if (shouldUnmount) {
prevChild.unmountComponent(shouldRemoveAllChildren);
}
}
// 1. When fragment embed fragment updated but prev fragment is empty
// that need to get the prev sibling native node.
// like: [ [] ] -> [ [1, 2] ]
// 2. When prev fragment is empty and update to other type
// like: [ [], 1 ] -> [ 1, 2 ]
if (isFragmentParent && parent.length === 0 || isPrevFirstEmptyFragment) {
lastPlacedNode = getPrevSiblingNativeNode(this);
}
}
if (nextChildren != null) {
// `nextIndex` will increment for each child in `nextChildren`
let nextIndex = 0;
function insertNodes(nativeNodes, parentNode) {
// The nativeNodes maybe fragment, so convert to array type
nativeNodes = toArray(nativeNodes);
for (let i = 0, l = nativeNodes.length; i < l; i++) {
if (lastPlacedNode) {
// Should reverse order when insert new child after lastPlacedNode:
// [lastPlacedNode, *newChild1, *newChild2],
// And if prev is empty fragment, lastPlacedNode is the prevSiblingNativeNode found.
driver.insertAfter(nativeNodes[l - 1 - i], lastPlacedNode);
} else if (prevFirstNativeNode) {
// [*newChild1, *newChild2, prevFirstNativeNode]
driver.insertBefore(nativeNodes[i], prevFirstNativeNode);
} else if (parentNode) {
// [*newChild1, *newChild2]
driver.appendChild(nativeNodes[i], parentNode);
}
}
}
for (let name in nextChildren) {
let nextChild = nextChildren[name];
let prevChild = prevChildren && prevChildren[name];
// Try to move the some key prevChild but current not at the some position
if (prevChild === nextChild) {
let prevChildNativeNode = prevChild.__getNativeNode();
if (prevChild.__mountIndex !== nextIndex) {
insertNodes(prevChildNativeNode);
}
} else {
// Mount nextChild that in prevChildren there has no some name
// Fragment extended native component, so if parent is fragment should get this._parent
if (isFragmentParent) {
parent = this._parent;
}
nextChild.__mountComponent(
parent,
this[INSTANCE],
context,
insertNodes // Insert nodes mounter
);
}
// Update to the latest mount order
nextChild.__mountIndex = nextIndex++;
// Get the last child
lastPlacedNode = nextChild.__getNativeNode();
if (isArray(lastPlacedNode)) {
lastPlacedNode = lastPlacedNode[lastPlacedNode.length - 1];
}
}
}
if (shouldUnmountPrevFirstChild) {
prevFirstChild.unmountComponent(shouldRemoveAllChildren);
}
if (shouldRemoveAllChildren) {
driver.removeChildren(this[NATIVE_NODE]);
}
this.__renderedChildren = nextChildren;
}
__createNativeNode() {
const instance = this[INSTANCE];
const nativeNode = Host.driver.createElement(instance.type, instance.props, this);
Instance.set(nativeNode, instance);
return nativeNode;
}
}