/** * Copyright (c) 2013-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /* eslint valid-typeof: 0 */ import invariant from 'shared/invariant'; import warning from 'shared/warning'; let didWarnForAddedNewProperty = false; const EVENT_POOL_SIZE = 10; const shouldBeReleasedProperties = [ 'dispatchConfig', '_targetInst', 'nativeEvent', 'isDefaultPrevented', 'isPropagationStopped', '_dispatchListeners', '_dispatchInstances', ]; /** * @interface Event * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ const EventInterface = { type: null, target: null, // currentTarget is set when dispatching; no use in copying it here currentTarget: function() { return null; }, eventPhase: null, bubbles: null, cancelable: null, timeStamp: function(event) { return event.timeStamp || Date.now(); }, defaultPrevented: null, isTrusted: null, }; function functionThatReturnsTrue() { return true; } function functionThatReturnsFalse() { return false; } /** * Synthetic events are dispatched by event plugins, typically in response to a * top-level event delegation handler. * * These systems should generally use pooling to reduce the frequency of garbage * collection. The system should check `isPersistent` to determine whether the * event should be released into the pool after being dispatched. Users that * need a persisted event should invoke `persist`. * * Synthetic events (and subclasses) implement the DOM Level 3 Events API by * normalizing browser quirks. Subclasses do not necessarily have to implement a * DOM interface; custom application-specific events can also subclass this. * * @param {object} dispatchConfig Configuration used to dispatch this event. * @param {*} targetInst Marker identifying the event target. * @param {object} nativeEvent Native browser event. * @param {DOMEventTarget} nativeEventTarget Target node. */ function SyntheticEvent( dispatchConfig, targetInst, nativeEvent, nativeEventTarget, ) { if (__DEV__) { // these have a getter/setter for warnings delete this.nativeEvent; delete this.preventDefault; delete this.stopPropagation; } this.dispatchConfig = dispatchConfig; this._targetInst = targetInst; this.nativeEvent = nativeEvent; const Interface = this.constructor.Interface; for (const propName in Interface) { if (!Interface.hasOwnProperty(propName)) { continue; } if (__DEV__) { delete this[propName]; // this has a getter/setter for warnings } const normalize = Interface[propName]; if (normalize) { this[propName] = normalize(nativeEvent); } else { if (propName === 'target') { this.target = nativeEventTarget; } else { this[propName] = nativeEvent[propName]; } } } const defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false; if (defaultPrevented) { this.isDefaultPrevented = functionThatReturnsTrue; } else { this.isDefaultPrevented = functionThatReturnsFalse; } this.isPropagationStopped = functionThatReturnsFalse; return this; } Object.assign(SyntheticEvent.prototype, { preventDefault: function() { this.defaultPrevented = true; const event = this.nativeEvent; if (!event) { return; } if (event.preventDefault) { event.preventDefault(); } else if (typeof event.returnValue !== 'unknown') { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; }, stopPropagation: function() { const event = this.nativeEvent; if (!event) { return; } if (event.stopPropagation) { event.stopPropagation(); } else if (typeof event.cancelBubble !== 'unknown') { // The ChangeEventPlugin registers a "propertychange" event for // IE. This event does not support bubbling or cancelling, and // any references to cancelBubble throw "Member not found". A // typeof check of "unknown" circumvents this issue (and is also // IE specific). event.cancelBubble = true; } this.isPropagationStopped = functionThatReturnsTrue; }, /** * We release all dispatched `SyntheticEvent`s after each event loop, adding * them back into the pool. This allows a way to hold onto a reference that * won't be added back into the pool. */ persist: function() { this.isPersistent = functionThatReturnsTrue; }, /** * Checks if this event should be released back into the pool. * * @return {boolean} True if this should not be released, false otherwise. */ isPersistent: functionThatReturnsFalse, /** * `PooledClass` looks for `destructor` on each instance it releases. */ destructor: function() { const Interface = this.constructor.Interface; for (const propName in Interface) { if (__DEV__) { Object.defineProperty( this, propName, getPooledWarningPropertyDefinition(propName, Interface[propName]), ); } else { this[propName] = null; } } for (let i = 0; i < shouldBeReleasedProperties.length; i++) { this[shouldBeReleasedProperties[i]] = null; } if (__DEV__) { Object.defineProperty( this, 'nativeEvent', getPooledWarningPropertyDefinition('nativeEvent', null), ); Object.defineProperty( this, 'preventDefault', getPooledWarningPropertyDefinition('preventDefault', () => {}), ); Object.defineProperty( this, 'stopPropagation', getPooledWarningPropertyDefinition('stopPropagation', () => {}), ); } }, }); SyntheticEvent.Interface = EventInterface; /** * Helper to reduce boilerplate when creating subclasses. */ SyntheticEvent.extend = function(Interface) { const Super = this; const E = function() {}; E.prototype = Super.prototype; const prototype = new E(); function Class() { return Super.apply(this, arguments); } Object.assign(prototype, Class.prototype); Class.prototype = prototype; Class.prototype.constructor = Class; Class.Interface = Object.assign({}, Super.Interface, Interface); Class.extend = Super.extend; addEventPoolingTo(Class); return Class; }; /** Proxying after everything set on SyntheticEvent * to resolve Proxy issue on some WebKit browsers * in which some Event properties are set to undefined (GH#10010) */ if (__DEV__) { const isProxySupported = typeof Proxy === 'function' && // https://github.com/facebook/react/issues/12011 !Object.isSealed(new Proxy({}, {})); if (isProxySupported) { /*eslint-disable no-func-assign */ SyntheticEvent = new Proxy(SyntheticEvent, { construct: function(target, args) { return this.apply(target, Object.create(target.prototype), args); }, apply: function(constructor, that, args) { return new Proxy(constructor.apply(that, args), { set: function(target, prop, value) { if ( prop !== 'isPersistent' && !target.constructor.Interface.hasOwnProperty(prop) && shouldBeReleasedProperties.indexOf(prop) === -1 ) { warning( didWarnForAddedNewProperty || target.isPersistent(), "This synthetic event is reused for performance reasons. If you're " + "seeing this, you're adding a new property in the synthetic event object. " + 'The property is never released. See ' + 'https://fb.me/react-event-pooling for more information.', ); didWarnForAddedNewProperty = true; } target[prop] = value; return true; }, }); }, }); /*eslint-enable no-func-assign */ } } addEventPoolingTo(SyntheticEvent); /** * Helper to nullify syntheticEvent instance properties when destructing * * @param {String} propName * @param {?object} getVal * @return {object} defineProperty object */ function getPooledWarningPropertyDefinition(propName, getVal) { const isFunction = typeof getVal === 'function'; return { configurable: true, set: set, get: get, }; function set(val) { const action = isFunction ? 'setting the method' : 'setting the property'; warn(action, 'This is effectively a no-op'); return val; } function get() { const action = isFunction ? 'accessing the method' : 'accessing the property'; const result = isFunction ? 'This is a no-op function' : 'This is set to null'; warn(action, result); return getVal; } function warn(action, result) { const warningCondition = false; warning( warningCondition, "This synthetic event is reused for performance reasons. If you're seeing this, " + "you're %s `%s` on a released/nullified synthetic event. %s. " + 'If you must keep the original synthetic event around, use event.persist(). ' + 'See https://fb.me/react-event-pooling for more information.', action, propName, result, ); } } function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) { const EventConstructor = this; if (EventConstructor.eventPool.length) { const instance = EventConstructor.eventPool.pop(); EventConstructor.call( instance, dispatchConfig, targetInst, nativeEvent, nativeInst, ); return instance; } return new EventConstructor( dispatchConfig, targetInst, nativeEvent, nativeInst, ); } function releasePooledEvent(event) { const EventConstructor = this; invariant( event instanceof EventConstructor, 'Trying to release an event instance into a pool of a different type.', ); event.destructor(); if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) { EventConstructor.eventPool.push(event); } } function addEventPoolingTo(EventConstructor) { EventConstructor.eventPool = []; EventConstructor.getPooled = getPooledEvent; EventConstructor.release = releasePooledEvent; } export default SyntheticEvent;