rax
Version:
A universal React-compatible render engine.
491 lines (416 loc) • 15.8 kB
JavaScript
import ReactiveComponent from './reactive';
import updater from './updater';
import Host from './host';
import { attachRef, updateRef, detachRef } from './ref';
import instantiateComponent from './instantiateComponent';
import shouldUpdateComponent from './shouldUpdateComponent';
import shallowEqual from './shallowEqual';
import BaseComponent from './base';
import getPrevSiblingNativeNode from './getPrevSiblingNativeNode';
import performInSandbox from './performInSandbox';
import toArray from '../toArray';
import { scheduleLayout } from './scheduler';
import { isFunction, isArray } from '../types';
import assign from '../assign';
import { INSTANCE, INTERNAL, RENDERED_COMPONENT } from '../constant';
import invokeFunctionsWithContext from '../invokeFunctionsWithContext';
import validateChildKeys from '../validateChildKeys';
import { throwError, throwMinifiedError } from '../error';
let measureLifeCycle;
if (process.env.NODE_ENV !== 'production') {
measureLifeCycle = function(callback, instanceID, type) {
Host.measurer && Host.measurer.beforeLifeCycle(instanceID, type);
callback();
Host.measurer && Host.measurer.afterLifeCycle(instanceID, type);
};
}
function scheduleLayoutInSandbox(fn, instance) {
scheduleLayout(() => {
performInSandbox(fn, instance);
});
}
function scheduleLayoutCallbacksInSandbox(callbacks, instance) {
if (callbacks) {
scheduleLayoutInSandbox(() => {
invokeFunctionsWithContext(callbacks, instance);
}, instance);
}
}
/**
* Composite Component
*/
class CompositeComponent extends BaseComponent {
__mountComponent(parent, parentInstance, context, nativeNodeMounter) {
this.__initComponent(parent, parentInstance, context);
if (process.env.NODE_ENV !== 'production') {
this._updateCount = 0;
Host.measurer && Host.measurer.beforeMountComponent(this._mountID, this);
}
let currentElement = this.__currentElement;
let Component = currentElement.type;
let ref = currentElement.ref;
let publicProps = currentElement.props;
let componentPrototype = Component.prototype;
// Context process
let publicContext = this.__processContext(context);
// Initialize the public class
let instance;
let renderedElement;
performInSandbox(() => {
if (componentPrototype && componentPrototype.render) {
// Class Component instance
instance = new Component(publicProps, publicContext);
} else if (isFunction(Component)) {
// Functional reactive component with hooks
instance = new ReactiveComponent(Component, ref);
} else {
if (process.env.NODE_ENV !== 'production') {
throwError('Invalid component type, expected a class or function component.', Component);
} else {
throwMinifiedError(6, Component);
}
}
}, parentInstance);
if (!instance) {
return;
}
// These should be set up in the constructor, but as a convenience for
// simpler class abstractions, we set them up after the fact.
instance.props = publicProps;
instance.context = publicContext;
instance.refs = {};
// Inject the updater into instance
instance.updater = updater;
instance[INTERNAL] = this;
this[INSTANCE] = instance;
// Init state, must be set to an object or null
let initialState = instance.state;
if (initialState === undefined) {
// TODO clone the state?
instance.state = initialState = null;
}
if (instance.componentWillMount) {
performInSandbox(() => {
if (process.env.NODE_ENV !== 'production') {
measureLifeCycle(() => {
instance.componentWillMount();
}, this._mountID, 'componentWillMount');
} else {
instance.componentWillMount();
}
}, instance);
}
Host.owner = this;
// Process pending state when call setState in componentWillMount
instance.state = this.__processPendingState(publicProps, publicContext);
const callbacks = this.__pendingCallbacks;
this.__pendingCallbacks = null;
performInSandbox(() => {
if (process.env.NODE_ENV !== 'production') {
measureLifeCycle(() => {
renderedElement = instance.render();
}, this._mountID, 'render');
} else {
renderedElement = instance.render();
}
}, instance);
if (process.env.NODE_ENV !== 'production') {
validateChildKeys(renderedElement, this.__currentElement.type);
}
Host.owner = null;
this[RENDERED_COMPONENT] = instantiateComponent(renderedElement);
this[RENDERED_COMPONENT].__mountComponent(
this._parent,
instance,
this.__processChildContext(context),
nativeNodeMounter
);
if (!currentElement.type._forwardRef && ref) {
attachRef(currentElement._owner, ref, this);
}
if (instance.componentDidMount) {
scheduleLayoutInSandbox(() => {
if (process.env.NODE_ENV !== 'production') {
measureLifeCycle(() => {
instance.componentDidMount();
}, this._mountID, 'componentDidMount');
} else {
instance.componentDidMount();
}
}, instance);
}
// Trigger setState callback
scheduleLayoutCallbacksInSandbox(callbacks, instance);
if (process.env.NODE_ENV !== 'production') {
scheduleLayout(() => {
Host.reconciler.mountComponent(this);
Host.measurer && Host.measurer.afterMountComponent(this._mountID);
});
}
return instance;
}
unmountComponent(shouldNotRemoveChild) {
let instance = this[INSTANCE];
// Unmounting a composite component maybe not complete mounted
// when throw error in component constructor stage
if (instance && instance.componentWillUnmount) {
performInSandbox(() => {
instance.componentWillUnmount();
}, instance);
}
if (this[RENDERED_COMPONENT] != null) {
let currentElement = this.__currentElement;
let ref = currentElement.ref;
if (!currentElement.type._forwardRef && ref) {
detachRef(currentElement._owner, ref, this);
}
this[RENDERED_COMPONENT].unmountComponent(shouldNotRemoveChild);
this[RENDERED_COMPONENT] = null;
}
// Reset pending fields
// Even if this component is scheduled for another async update,
// it would still be ignored because these fields are reset.
this.__pendingStateQueue = null;
this.__isPendingForceUpdate = false;
this.__destoryComponent();
}
/**
* Filters the context object to only contain keys specified in
* `contextTypes`
*/
__processContext(context) {
let maskedContext = {};
let Component = this.__currentElement.type;
let contextTypes = Component.contextTypes;
if (contextTypes) {
for (let contextName in contextTypes) {
maskedContext[contextName] = context[contextName];
}
}
return maskedContext;
}
__processChildContext(currentContext) {
let instance = this[INSTANCE];
// The getChildContext method context should be current instance
let childContext = instance.getChildContext && instance.getChildContext();
if (childContext) {
return assign({}, currentContext, childContext);
}
return currentContext;
}
__processPendingState(props, context) {
let instance = this[INSTANCE];
let queue = this.__pendingStateQueue;
if (!queue) {
return instance.state;
}
// Reset pending queue
this.__pendingStateQueue = null;
let nextState = assign({}, instance.state);
for (let i = 0; i < queue.length; i++) {
let partial = queue[i];
assign(
nextState,
isFunction(partial) ?
partial.call(instance, nextState, props, context) :
partial
);
}
return nextState;
}
__updateComponent(
prevElement,
nextElement,
prevUnmaskedContext,
nextUnmaskedContext
) {
let instance = this[INSTANCE];
// Maybe update component that has already been unmounted or failed mount.
if (!instance) {
return;
}
performInSandbox(() => {
if (process.env.NODE_ENV !== 'production') {
Host.measurer && Host.measurer.beforeUpdateComponent(this._mountID, this);
}
let willReceive;
let nextContext;
let nextProps;
// Determine if the context has changed or not
if (this._context === nextUnmaskedContext) {
nextContext = instance.context;
} else {
nextContext = this.__processContext(nextUnmaskedContext);
willReceive = true;
}
// Distinguish between a props update versus a simple state update
// Skip checking prop types again -- we don't read component.props to avoid
// warning for DOM component props in this upgrade
nextProps = nextElement.props;
if (prevElement !== nextElement) {
willReceive = true;
}
if (willReceive && instance.componentWillReceiveProps) {
// Calling this.setState() within componentWillReceiveProps will not trigger an additional render.
this.__isPendingState = true;
instance.componentWillReceiveProps(nextProps, nextContext);
this.__isPendingState = false;
}
// Update refs
if (this.__currentElement.type._forwardRef) {
instance.__prevForwardRef = prevElement.ref;
instance._forwardRef = nextElement.ref;
} else {
updateRef(prevElement, nextElement, this);
}
// Shoud update default
let shouldUpdate = true;
let prevProps = instance.props;
let prevState = instance.state;
// TODO: could delay execution processPendingState
let nextState = this.__processPendingState(nextProps, nextContext);
const callbacks = this.__pendingCallbacks;
this.__pendingCallbacks = null;
// ShouldComponentUpdate is not called when forceUpdate is used
if (!this.__isPendingForceUpdate) {
if (instance.shouldComponentUpdate) {
shouldUpdate = instance.shouldComponentUpdate(nextProps, nextState, nextContext);
} else if (instance.__isPureComponent) {
// Pure Component
shouldUpdate = !shallowEqual(prevProps, nextProps) ||
!shallowEqual(prevState, nextState);
}
}
if (shouldUpdate) {
this.__isPendingForceUpdate = false;
// Will set `this.props`, `this.state` and `this.context`.
let prevContext = instance.context;
// Cannot use this.setState() in componentWillUpdate.
// If need to update state in response to a prop change, use componentWillReceiveProps instead.
if (instance.componentWillUpdate) {
instance.componentWillUpdate(nextProps, nextState, nextContext);
}
// Replace with next
this.__currentElement = nextElement;
this._context = nextUnmaskedContext;
instance.props = nextProps;
instance.state = nextState;
instance.context = nextContext;
this.__updateRenderedComponent(nextUnmaskedContext);
if (instance.componentDidUpdate) {
scheduleLayoutInSandbox(() => {
instance.componentDidUpdate(prevProps, prevState, prevContext);
}, instance);
}
if (process.env.NODE_ENV !== 'production') {
// Calc update count.
this._updateCount++;
}
} else {
// If it's determined that a component should not update, we still want
// to set props and state but we shortcut the rest of the update.
this.__currentElement = nextElement;
this._context = nextUnmaskedContext;
instance.props = nextProps;
instance.state = nextState;
instance.context = nextContext;
}
scheduleLayoutCallbacksInSandbox(callbacks, instance);
if (process.env.NODE_ENV !== 'production') {
scheduleLayout(() => {
Host.measurer && Host.measurer.afterUpdateComponent(this._mountID);
Host.reconciler.receiveComponent(this);
});
}
}, instance);
}
/**
* Call the component's `render` method and update the DOM accordingly.
*/
__updateRenderedComponent(context) {
let prevRenderedComponent = this[RENDERED_COMPONENT];
let prevRenderedElement = prevRenderedComponent.__currentElement;
let instance = this[INSTANCE];
let nextRenderedElement;
Host.owner = this;
if (process.env.NODE_ENV !== 'production') {
measureLifeCycle(() => {
nextRenderedElement = instance.render();
}, this._mountID, 'render');
} else {
nextRenderedElement = instance.render();
}
Host.owner = null;
if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) {
const prevRenderedUnmaskedContext = prevRenderedComponent._context;
const nextRenderedUnmaskedContext = this.__processChildContext(context);
// If getChildContext existed and invoked when component updated that will make
// prevRenderedUnmaskedContext not equal nextRenderedUnmaskedContext under the tree
if (prevRenderedElement !== nextRenderedElement || prevRenderedUnmaskedContext !== nextRenderedUnmaskedContext) {
// If element type is illegal catch the error
prevRenderedComponent.__updateComponent(
prevRenderedElement,
nextRenderedElement,
prevRenderedUnmaskedContext,
nextRenderedUnmaskedContext
);
}
if (process.env.NODE_ENV !== 'production') {
Host.measurer && Host.measurer.recordOperation({
instanceID: this._mountID,
type: 'update component',
payload: {}
});
}
} else {
let lastNativeNode = null;
let prevNativeNode = prevRenderedComponent.__getNativeNode();
// Only prevNativeNode is empty fragment should find the prevSlibingNativeNode
// And current root component is fragment, but not need find the prevSlibingNativeNode when init mounting
if (isArray(prevNativeNode) && prevNativeNode.length === 0 && instance.__rootID == null) {
lastNativeNode = getPrevSiblingNativeNode(prevRenderedComponent);
}
prevRenderedComponent.unmountComponent(true);
this[RENDERED_COMPONENT] = instantiateComponent(nextRenderedElement);
this[RENDERED_COMPONENT].__mountComponent(
this._parent,
instance,
this.__processChildContext(context),
(newNativeNode, parent) => {
const driver = Host.driver;
prevNativeNode = toArray(prevNativeNode);
newNativeNode = toArray(newNativeNode);
// If the new length large then prev
for (let i = 0; i < newNativeNode.length; i++) {
let nativeNode = newNativeNode[i];
if (prevNativeNode[i]) {
driver.replaceChild(nativeNode, prevNativeNode[i]);
} else if (lastNativeNode) {
driver.insertAfter(nativeNode, lastNativeNode);
} else {
driver.appendChild(nativeNode, parent);
}
lastNativeNode = nativeNode;
}
// If the new length less then prev
for (let i = newNativeNode.length; i < prevNativeNode.length; i++) {
driver.removeChild(prevNativeNode[i]);
}
}
);
}
}
__getNativeNode() {
let renderedComponent = this[RENDERED_COMPONENT];
if (renderedComponent) {
return renderedComponent.__getNativeNode();
}
}
__getPublicInstance() {
let instance = this[INSTANCE];
// The functional components cannot be given refs
if (instance.__isReactiveComponent) return null;
return instance;
}
}
export default CompositeComponent;