mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
387 lines
11 KiB
JavaScript
387 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
import React from 'react';
|
|
import {isForwardRef} from 'react-is';
|
|
import describeComponentFrame from 'shared/describeComponentFrame';
|
|
import getComponentName from 'shared/getComponentName';
|
|
import shallowEqual from 'shared/shallowEqual';
|
|
import invariant from 'shared/invariant';
|
|
import checkPropTypes from 'prop-types/checkPropTypes';
|
|
|
|
const emptyObject = {};
|
|
if (__DEV__) {
|
|
Object.freeze(emptyObject);
|
|
}
|
|
|
|
class Updater {
|
|
constructor(renderer) {
|
|
this._renderer = renderer;
|
|
this._callbacks = [];
|
|
}
|
|
|
|
_enqueueCallback(callback, publicInstance) {
|
|
if (typeof callback === 'function' && publicInstance) {
|
|
this._callbacks.push({
|
|
callback,
|
|
publicInstance,
|
|
});
|
|
}
|
|
}
|
|
|
|
_invokeCallbacks() {
|
|
const callbacks = this._callbacks;
|
|
this._callbacks = [];
|
|
|
|
callbacks.forEach(({callback, publicInstance}) => {
|
|
callback.call(publicInstance);
|
|
});
|
|
}
|
|
|
|
isMounted(publicInstance) {
|
|
return !!this._renderer._element;
|
|
}
|
|
|
|
enqueueForceUpdate(publicInstance, callback, callerName) {
|
|
this._enqueueCallback(callback, publicInstance);
|
|
this._renderer._forcedUpdate = true;
|
|
this._renderer.render(this._renderer._element, this._renderer._context);
|
|
}
|
|
|
|
enqueueReplaceState(publicInstance, completeState, callback, callerName) {
|
|
this._enqueueCallback(callback, publicInstance);
|
|
this._renderer._newState = completeState;
|
|
this._renderer.render(this._renderer._element, this._renderer._context);
|
|
}
|
|
|
|
enqueueSetState(publicInstance, partialState, callback, callerName) {
|
|
this._enqueueCallback(callback, publicInstance);
|
|
const currentState = this._renderer._newState || publicInstance.state;
|
|
|
|
if (typeof partialState === 'function') {
|
|
partialState = partialState.call(
|
|
publicInstance,
|
|
currentState,
|
|
publicInstance.props,
|
|
);
|
|
}
|
|
|
|
// Null and undefined are treated as no-ops.
|
|
if (partialState === null || partialState === undefined) {
|
|
return;
|
|
}
|
|
|
|
this._renderer._newState = {
|
|
...currentState,
|
|
...partialState,
|
|
};
|
|
|
|
this._renderer.render(this._renderer._element, this._renderer._context);
|
|
}
|
|
}
|
|
|
|
class ReactShallowRenderer {
|
|
static createRenderer = function() {
|
|
return new ReactShallowRenderer();
|
|
};
|
|
|
|
constructor() {
|
|
this._context = null;
|
|
this._element = null;
|
|
this._instance = null;
|
|
this._newState = null;
|
|
this._rendered = null;
|
|
this._rendering = false;
|
|
this._forcedUpdate = false;
|
|
this._updater = new Updater(this);
|
|
}
|
|
|
|
getMountedInstance() {
|
|
return this._instance;
|
|
}
|
|
|
|
getRenderOutput() {
|
|
return this._rendered;
|
|
}
|
|
|
|
render(element, context = emptyObject) {
|
|
invariant(
|
|
React.isValidElement(element),
|
|
'ReactShallowRenderer render(): Invalid component element.%s',
|
|
typeof element === 'function'
|
|
? ' Instead of passing a component class, make sure to instantiate ' +
|
|
'it by passing it to React.createElement.'
|
|
: '',
|
|
);
|
|
// Show a special message for host elements since it's a common case.
|
|
invariant(
|
|
typeof element.type !== 'string',
|
|
'ReactShallowRenderer render(): Shallow rendering works only with custom ' +
|
|
'components, not primitives (%s). Instead of calling `.render(el)` and ' +
|
|
'inspecting the rendered output, look at `el.props` directly instead.',
|
|
element.type,
|
|
);
|
|
invariant(
|
|
isForwardRef(element) || typeof element.type === 'function',
|
|
'ReactShallowRenderer render(): Shallow rendering works only with custom ' +
|
|
'components, but the provided element type was `%s`.',
|
|
Array.isArray(element.type)
|
|
? 'array'
|
|
: element.type === null
|
|
? 'null'
|
|
: typeof element.type,
|
|
);
|
|
|
|
if (this._rendering) {
|
|
return;
|
|
}
|
|
|
|
this._rendering = true;
|
|
this._element = element;
|
|
this._context = getMaskedContext(element.type.contextTypes, context);
|
|
|
|
if (this._instance) {
|
|
this._updateClassComponent(element, this._context);
|
|
} else {
|
|
if (isForwardRef(element)) {
|
|
this._rendered = element.type.render(element.props, element.ref);
|
|
} else if (shouldConstruct(element.type)) {
|
|
this._instance = new element.type(
|
|
element.props,
|
|
this._context,
|
|
this._updater,
|
|
);
|
|
|
|
this._updateStateFromStaticLifecycle(element.props);
|
|
|
|
if (element.type.hasOwnProperty('contextTypes')) {
|
|
currentlyValidatingElement = element;
|
|
|
|
checkPropTypes(
|
|
element.type.contextTypes,
|
|
this._context,
|
|
'context',
|
|
getName(element.type, this._instance),
|
|
getStackAddendum,
|
|
);
|
|
|
|
currentlyValidatingElement = null;
|
|
}
|
|
|
|
this._mountClassComponent(element, this._context);
|
|
} else {
|
|
this._rendered = element.type.call(
|
|
undefined,
|
|
element.props,
|
|
this._context,
|
|
);
|
|
}
|
|
}
|
|
|
|
this._rendering = false;
|
|
this._updater._invokeCallbacks();
|
|
|
|
return this.getRenderOutput();
|
|
}
|
|
|
|
unmount() {
|
|
if (this._instance) {
|
|
if (typeof this._instance.componentWillUnmount === 'function') {
|
|
this._instance.componentWillUnmount();
|
|
}
|
|
}
|
|
|
|
this._context = null;
|
|
this._element = null;
|
|
this._newState = null;
|
|
this._rendered = null;
|
|
this._instance = null;
|
|
}
|
|
|
|
_mountClassComponent(element, context) {
|
|
this._instance.context = context;
|
|
this._instance.props = element.props;
|
|
this._instance.state = this._instance.state || null;
|
|
this._instance.updater = this._updater;
|
|
|
|
if (
|
|
typeof this._instance.UNSAFE_componentWillMount === 'function' ||
|
|
typeof this._instance.componentWillMount === 'function'
|
|
) {
|
|
const beforeState = this._newState;
|
|
|
|
// In order to support react-lifecycles-compat polyfilled components,
|
|
// Unsafe lifecycles should not be invoked for components using the new APIs.
|
|
if (
|
|
typeof element.type.getDerivedStateFromProps !== 'function' &&
|
|
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
|
|
) {
|
|
if (typeof this._instance.componentWillMount === 'function') {
|
|
this._instance.componentWillMount();
|
|
}
|
|
if (typeof this._instance.UNSAFE_componentWillMount === 'function') {
|
|
this._instance.UNSAFE_componentWillMount();
|
|
}
|
|
}
|
|
|
|
// setState may have been called during cWM
|
|
if (beforeState !== this._newState) {
|
|
this._instance.state = this._newState || emptyObject;
|
|
}
|
|
}
|
|
|
|
this._rendered = this._instance.render();
|
|
// Intentionally do not call componentDidMount()
|
|
// because DOM refs are not available.
|
|
}
|
|
|
|
_updateClassComponent(element, context) {
|
|
const {props, type} = element;
|
|
|
|
const oldState = this._instance.state || emptyObject;
|
|
const oldProps = this._instance.props;
|
|
|
|
if (oldProps !== props) {
|
|
// In order to support react-lifecycles-compat polyfilled components,
|
|
// Unsafe lifecycles should not be invoked for components using the new APIs.
|
|
if (
|
|
typeof element.type.getDerivedStateFromProps !== 'function' &&
|
|
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
|
|
) {
|
|
if (typeof this._instance.componentWillReceiveProps === 'function') {
|
|
this._instance.componentWillReceiveProps(props, context);
|
|
}
|
|
if (
|
|
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
|
|
) {
|
|
this._instance.UNSAFE_componentWillReceiveProps(props, context);
|
|
}
|
|
}
|
|
}
|
|
this._updateStateFromStaticLifecycle(props);
|
|
|
|
// Read state after cWRP in case it calls setState
|
|
const state = this._newState || oldState;
|
|
|
|
let shouldUpdate = true;
|
|
if (this._forcedUpdate) {
|
|
shouldUpdate = true;
|
|
this._forcedUpdate = false;
|
|
} else if (typeof this._instance.shouldComponentUpdate === 'function') {
|
|
shouldUpdate = !!this._instance.shouldComponentUpdate(
|
|
props,
|
|
state,
|
|
context,
|
|
);
|
|
} else if (type.prototype && type.prototype.isPureReactComponent) {
|
|
shouldUpdate =
|
|
!shallowEqual(oldProps, props) || !shallowEqual(oldState, state);
|
|
}
|
|
|
|
if (shouldUpdate) {
|
|
// In order to support react-lifecycles-compat polyfilled components,
|
|
// Unsafe lifecycles should not be invoked for components using the new APIs.
|
|
if (
|
|
typeof element.type.getDerivedStateFromProps !== 'function' &&
|
|
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
|
|
) {
|
|
if (typeof this._instance.componentWillUpdate === 'function') {
|
|
this._instance.componentWillUpdate(props, state, context);
|
|
}
|
|
if (typeof this._instance.UNSAFE_componentWillUpdate === 'function') {
|
|
this._instance.UNSAFE_componentWillUpdate(props, state, context);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._instance.context = context;
|
|
this._instance.props = props;
|
|
this._instance.state = state;
|
|
|
|
if (shouldUpdate) {
|
|
this._rendered = this._instance.render();
|
|
}
|
|
// Intentionally do not call componentDidUpdate()
|
|
// because DOM refs are not available.
|
|
}
|
|
|
|
_updateStateFromStaticLifecycle(props) {
|
|
const {type} = this._element;
|
|
|
|
if (typeof type.getDerivedStateFromProps === 'function') {
|
|
const oldState = this._newState || this._instance.state;
|
|
const partialState = type.getDerivedStateFromProps.call(
|
|
null,
|
|
props,
|
|
oldState,
|
|
);
|
|
|
|
if (partialState != null) {
|
|
const newState = Object.assign({}, oldState, partialState);
|
|
this._instance.state = this._newState = newState;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let currentlyValidatingElement = null;
|
|
|
|
function getDisplayName(element) {
|
|
if (element == null) {
|
|
return '#empty';
|
|
} else if (typeof element === 'string' || typeof element === 'number') {
|
|
return '#text';
|
|
} else if (typeof element.type === 'string') {
|
|
return element.type;
|
|
} else {
|
|
return element.type.displayName || element.type.name || 'Unknown';
|
|
}
|
|
}
|
|
|
|
function getStackAddendum() {
|
|
let stack = '';
|
|
if (currentlyValidatingElement) {
|
|
const name = getDisplayName(currentlyValidatingElement);
|
|
const owner = currentlyValidatingElement._owner;
|
|
stack += describeComponentFrame(
|
|
name,
|
|
currentlyValidatingElement._source,
|
|
owner && getComponentName(owner.type),
|
|
);
|
|
}
|
|
return stack;
|
|
}
|
|
|
|
function getName(type, instance) {
|
|
const constructor = instance && instance.constructor;
|
|
return (
|
|
type.displayName ||
|
|
(constructor && constructor.displayName) ||
|
|
type.name ||
|
|
(constructor && constructor.name) ||
|
|
null
|
|
);
|
|
}
|
|
|
|
function shouldConstruct(Component) {
|
|
return !!(Component.prototype && Component.prototype.isReactComponent);
|
|
}
|
|
|
|
function getMaskedContext(contextTypes, unmaskedContext) {
|
|
if (!contextTypes) {
|
|
return emptyObject;
|
|
}
|
|
const context = {};
|
|
for (let key in contextTypes) {
|
|
context[key] = unmaskedContext[key];
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export default ReactShallowRenderer;
|