Files
react/src/hook.js
T
2019-05-20 09:39:26 -07:00

204 lines
5.9 KiB
JavaScript

/**
* Install the hook on window, which is an event emitter.
* Note because Chrome content scripts cannot directly modify the window object,
* we are evaling this function by inserting a script tag.
* That's why we have to inline the whole event emitter implementation here.
*
* @flow
*/
import type { DevToolsHook } from 'src/backend/types';
declare var window: any;
export function installHook(target: any): DevToolsHook | null {
if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) return null;
function detectReactBuildType(renderer) {
try {
if (typeof renderer.version === 'string') {
if (renderer.bundleType > 0) {
// This is not a production build.
// We are currently only using 0 (PROD) and 1 (DEV)
// but might add 2 (PROFILE) in the future.
return 'development';
}
// React 16 uses flat bundles. If we report the bundle as production
// version, it means we also minified and envified it ourselves.
return 'production';
// Note: There is still a risk that the CommonJS entry point has not
// been envified or uglified. In this case the user would have *both*
// development and production bundle, but only the prod one would run.
// This would be really bad. We have a separate check for this because
// it happens *outside* of the renderer injection. See `checkDCE` below.
}
} catch (err) {}
return 'production';
}
function checkDCE(fn: Function) {
// This runs for production versions of React.
// Needs to be super safe.
try {
const toString = Function.prototype.toString;
const code = toString.call(fn);
// This is a string embedded in the passed function under DEV-only
// condition. However the function executes only in PROD. Therefore,
// if we see it, dead code elimination did not work.
if (code.indexOf('^_^') > -1) {
// Remember to report during next injection.
hasDetectedBadDCE = true;
// Bonus: throw an exception hoping that it gets picked up by a reporting system.
// Not synchronously so that it doesn't break the calling code.
setTimeout(function() {
throw new Error(
'React is running in production mode, but dead code ' +
'elimination has not been applied. Read how to correctly ' +
'configure React for production: ' +
'https://fb.me/react-perf-use-the-production-build'
);
});
}
} catch (err) {}
}
let uidCounter = 0;
function inject(renderer) {
const id = ++uidCounter;
renderers.set(id, renderer);
const reactBuildType = hasDetectedBadDCE
? 'deadcode'
: detectReactBuildType(renderer);
// If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
// Otherwise the renderer won't yet exist and we can skip this step.
const attach = target.__REACT_DEVTOOLS_ATTACH__;
if (typeof attach === 'function') {
const rendererInterface = attach(hook, id, renderer, target);
hook.rendererInterfaces.set(id, rendererInterface);
}
hook.emit('renderer', { id, renderer, reactBuildType });
return id;
}
let hasDetectedBadDCE = false;
function sub(event, fn) {
hook.on(event, fn);
return () => hook.off(event, fn);
}
function on(event, fn) {
if (!listeners[event]) {
listeners[event] = [];
}
listeners[event].push(fn);
}
function off(event, fn) {
if (!listeners[event]) {
return;
}
const index = listeners[event].indexOf(fn);
if (index !== -1) {
listeners[event].splice(index, 1);
}
if (!listeners[event].length) {
delete listeners[event];
}
}
function emit(event, data) {
if (listeners[event]) {
listeners[event].map(fn => fn(data));
}
}
function getFiberRoots(rendererID) {
const roots = fiberRoots;
if (!roots[rendererID]) {
roots[rendererID] = new Set();
}
return roots[rendererID];
}
function onCommitFiberUnmount(rendererID, fiber) {
const rendererInterface = rendererInterfaces.get(rendererID);
if (rendererInterface != null) {
rendererInterface.handleCommitFiberUnmount(fiber);
}
}
function onCommitFiberRoot(rendererID, root, priorityLevel) {
const mountedRoots = hook.getFiberRoots(rendererID);
const current = root.current;
const isKnownRoot = mountedRoots.has(root);
const isUnmounting =
current.memoizedState == null || current.memoizedState.element == null;
// Keep track of mounted roots so we can hydrate when DevTools connect.
if (!isKnownRoot && !isUnmounting) {
mountedRoots.add(root);
} else if (isKnownRoot && isUnmounting) {
mountedRoots.delete(root);
}
const rendererInterface = rendererInterfaces.get(rendererID);
if (rendererInterface != null) {
rendererInterface.handleCommitFiberRoot(root, priorityLevel);
}
}
// TODO: More meaningful names for "rendererInterfaces" and "renderers".
const fiberRoots = {};
const rendererInterfaces = new Map();
const listeners = {};
const renderers = new Map();
const hook: DevToolsHook = {
rendererInterfaces,
listeners,
renderers,
emit,
getFiberRoots,
inject,
on,
off,
sub,
// This is a legacy flag.
// React v16 checks the hook for this to ensure DevTools is new enough.
supportsFiber: true,
// React calls these methods.
checkDCE,
onCommitFiberUnmount,
onCommitFiberRoot,
};
Object.defineProperty(
target,
'__REACT_DEVTOOLS_GLOBAL_HOOK__',
({
// This property needs to be configurable for the test environment,
// else we won't be able to delete and recreate it beween tests.
configurable: __DEV__,
enumerable: false,
get() {
return hook;
},
}: Object)
);
return hook;
}