From 0db4077c3ac7c914c30169cce6ab7ddfb8aec3c9 Mon Sep 17 00:00:00 2001 From: Pete Hunt Date: Thu, 29 Aug 2013 17:32:36 -0700 Subject: [PATCH] Make React batching strategy injectable --- src/core/ReactDefaultBatchingStrategy.js | 74 ++++++++++++++++ src/core/ReactDefaultInjection.js | 7 ++ src/core/ReactUpdates.js | 102 ++++++++++++++--------- 3 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 src/core/ReactDefaultBatchingStrategy.js diff --git a/src/core/ReactDefaultBatchingStrategy.js b/src/core/ReactDefaultBatchingStrategy.js new file mode 100644 index 0000000000..242a77aa97 --- /dev/null +++ b/src/core/ReactDefaultBatchingStrategy.js @@ -0,0 +1,74 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ReactDefaultBatchingStrategy + */ + +"use strict"; + +var ReactUpdates = require('ReactUpdates'); +var Transaction = require('Transaction'); + +var emptyFunction = require('emptyFunction'); +var mixInto = require('mixInto'); + +var RESET_BATCHED_UPDATES = { + initialize: emptyFunction, + close: function() { + ReactDefaultBatchingStrategy.isBatchingUpdates = false; + } +}; + +var FLUSH_BATCHED_UPDATES = { + initialize: emptyFunction, + close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates) +}; + +var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; + +function ReactDefaultBatchingStrategyTransaction() { + this.reinitializeTransaction(); +} + +mixInto(ReactDefaultBatchingStrategyTransaction, Transaction.Mixin); +mixInto(ReactDefaultBatchingStrategyTransaction, { + getTransactionWrappers: function() { + return TRANSACTION_WRAPPERS; + } +}); + +var ReactDefaultBatchingStrategy = { + isBatchingUpdates: false, + + /** + * Call the provided function in a context within which calls to `setState` + * and friends are batched such that components aren't updated unnecessarily. + */ + batchedUpdates: function(callback) { + var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; + + ReactDefaultBatchingStrategy.isBatchingUpdates = true; + + // The code is written this way to avoid extra allocations + if (alreadyBatchingUpdates) { + callback(); + } else { + var transaction = new ReactDefaultBatchingStrategyTransaction(); + transaction.perform(callback); + } + } +}; + +module.exports = ReactDefaultBatchingStrategy; diff --git a/src/core/ReactDefaultInjection.js b/src/core/ReactDefaultInjection.js index b1a0d90ac1..903806443e 100644 --- a/src/core/ReactDefaultInjection.js +++ b/src/core/ReactDefaultInjection.js @@ -40,6 +40,9 @@ var ReactInstanceHandles = require('ReactInstanceHandles'); var SimpleEventPlugin = require('SimpleEventPlugin'); var MobileSafariClickEventPlugin = require('MobileSafariClickEventPlugin'); +var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); +var ReactUpdates = require('ReactUpdates'); + function inject() { ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback; /** @@ -73,6 +76,10 @@ function inject() { if (__DEV__) { ReactPerf.injection.injectMeasure(require('ReactDefaultPerf').measure); } + + ReactUpdates.injection.injectBatchingStrategy( + ReactDefaultBatchingStrategy + ); } module.exports = { diff --git a/src/core/ReactUpdates.js b/src/core/ReactUpdates.js index c0b5317853..9be22c4849 100644 --- a/src/core/ReactUpdates.js +++ b/src/core/ReactUpdates.js @@ -20,51 +20,54 @@ var invariant = require('invariant'); -var isBatchingUpdates = false; - var dirtyComponents = []; -/** - * Call the provided function in a context within which calls to `setState` and - * friends are batched such that components aren't updated unnecessarily. - */ +var batchingStrategy = null; + +function ensureBatchingStrategy() { + invariant(batchingStrategy, 'ReactUpdates: must inject a batching strategy'); +} + function batchedUpdates(callback) { - if (isBatchingUpdates) { - // We're already executing in an environment where updates will be batched, - // so this is a no-op. - callback(); - return; - } + ensureBatchingStrategy(); + batchingStrategy.batchedUpdates(callback); +} - isBatchingUpdates = true; - - try { - callback(); - // TODO: Sort components by depth such that parent components update first - for (var i = 0; i < dirtyComponents.length; i++) { - // If a component is unmounted before pending changes apply, ignore them - // TODO: Queue unmounts in the same list to avoid this happening at all - var component = dirtyComponents[i]; - if (component.isMounted()) { - // If performUpdateIfNecessary happens to enqueue any new updates, we - // shouldn't execute the callbacks until the next render happens, so - // stash the callbacks first - var callbacks = component._pendingCallbacks; - component._pendingCallbacks = null; - component.performUpdateIfNecessary(); - if (callbacks) { - for (var j = 0; j < callbacks.length; j++) { - callbacks[j].call(component); - } +function runBatchedUpdates() { + // TODO: Sort components by depth such that parent components update first + for (var i = 0; i < dirtyComponents.length; i++) { + // If a component is unmounted before pending changes apply, ignore them + // TODO: Queue unmounts in the same list to avoid this happening at all + var component = dirtyComponents[i]; + if (component.isMounted()) { + // If performUpdateIfNecessary happens to enqueue any new updates, we + // shouldn't execute the callbacks until the next render happens, so + // stash the callbacks first + var callbacks = component._pendingCallbacks; + component._pendingCallbacks = null; + component.performUpdateIfNecessary(); + if (callbacks) { + for (var j = 0; j < callbacks.length; j++) { + callbacks[j].call(component); } } } - } catch (error) { - // IE8 requires `catch` in order to use `finally`. - throw error; + } +} + +function clearDirtyComponents() { + dirtyComponents.length = 0; +} + +function flushBatchedUpdates() { + // Run these in separate functions so the JIT can optimize + try { + runBatchedUpdates(); + } catch (e) { + // IE 8 requires catch to use finally. + throw e; } finally { - dirtyComponents.length = 0; - isBatchingUpdates = false; + clearDirtyComponents(); } } @@ -79,8 +82,9 @@ function enqueueUpdate(component, callback) { '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.' ); + ensureBatchingStrategy(); - if (!isBatchingUpdates) { + if (!batchingStrategy.isBatchingUpdates) { component.performUpdateIfNecessary(); callback && callback(); return; @@ -97,9 +101,29 @@ function enqueueUpdate(component, callback) { } } +var ReactUpdatesInjection = { + injectBatchingStrategy: function(_batchingStrategy) { + invariant( + _batchingStrategy, + 'ReactUpdates: must provide a batching strategy' + ); + invariant( + typeof _batchingStrategy.batchedUpdates === 'function', + 'ReactUpdates: must provide a batchedUpdates() function' + ); + invariant( + typeof _batchingStrategy.isBatchingUpdates === 'boolean', + 'ReactUpdates: must provide an isBatchingUpdates boolean attribute' + ); + batchingStrategy = _batchingStrategy; + } +}; + var ReactUpdates = { batchedUpdates: batchedUpdates, - enqueueUpdate: enqueueUpdate + enqueueUpdate: enqueueUpdate, + flushBatchedUpdates: flushBatchedUpdates, + injection: ReactUpdatesInjection }; module.exports = ReactUpdates;