diff --git a/src/addons/ReactWithAddons.js b/src/addons/ReactWithAddons.js index bcf5f06ceaab2..c74144737fb26 100644 --- a/src/addons/ReactWithAddons.js +++ b/src/addons/ReactWithAddons.js @@ -23,6 +23,7 @@ var React = require('React'); var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); var ReactCSSTransitionGroup = require('ReactCSSTransitionGroup'); +var ReactDataTracker = require('ReactDataTracker'); var ReactFragment = require('ReactFragment'); var ReactTransitionGroup = require('ReactTransitionGroup'); var ReactUpdates = require('ReactUpdates'); @@ -43,7 +44,15 @@ React.addons = { createFragment: ReactFragment.create, renderSubtreeIntoContainer: renderSubtreeIntoContainer, shallowCompare: shallowCompare, - update: update + update: update, + observeRead: function(reactDataEntity) { + ReactDataTracker.startRead(reactDataEntity); + ReactDataTracker.endRead(reactDataEntity); + }, + observeWrite: function(reactDataEntity) { + ReactDataTracker.startWrite(reactDataEntity); + ReactDataTracker.endWrite(reactDataEntity); + } }; if (__DEV__) { diff --git a/src/addons/__tests__/ReactDataTracker-test.js b/src/addons/__tests__/ReactDataTracker-test.js new file mode 100644 index 0000000000000..7e44dfd851487 --- /dev/null +++ b/src/addons/__tests__/ReactDataTracker-test.js @@ -0,0 +1,51 @@ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +var React = require('ReactWithAddons'); + +describe('ReactDataTrack', function() { + + it('should update component when a write fires', function () { + + class Person { + constructor(name) { + this.setName(name); + } + + setName(name) { + this.name = name; + React.addons.observeWrite(this); + } + + getName() { + React.addons.observeRead(this); + return this.name; + } + } + + class PersonView extends React.Component { + render() { + return
{this.props.person.getName()}
; + } + } + + var container = document.createElement('div'); + + var person = new Person("jimfb"); + React.render(, container); + expect(container.children[0].innerHTML).toBe('jimfb'); + person.setName("Jim"); + expect(container.children[0].innerHTML).toBe('Jim'); + React.unmountComponentAtNode(container); + }); +}); diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index 090ae1624ca44..96d6721337a77 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -698,6 +698,9 @@ var ReactMount = { ); delete instancesByReactRootID[reactRootID]; delete containersByReactRootID[reactRootID]; + if (component._instance._tracker) { + component._instance._tracker.destroy(); + } if (__DEV__) { delete rootElementsByReactRootID[reactRootID]; } diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js index 1f6b2a635c4d2..539e20af7e571 100644 --- a/src/renderers/shared/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js @@ -14,6 +14,7 @@ var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactContext = require('ReactContext'); var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactDataTracker = require('ReactDataTracker'); var ReactElement = require('ReactElement'); var ReactElementValidator = require('ReactElementValidator'); var ReactInstanceMap = require('ReactInstanceMap'); @@ -739,7 +740,20 @@ var ReactCompositeComponentMixin = { */ _renderValidatedComponentWithoutOwnerOrContext: function() { var inst = this._instance; - var renderedComponent = inst.render(); + + // Setup data tracker (TODO: Singleton tracker for faster perf) + if (inst._tracker === undefined) { + inst._tracker = new ReactDataTracker(function() { + return inst.render(); + }); + inst._tracker.setCallback(function() { + inst.setState({}); + }); + } else { + inst._tracker._cacheValid = false; + } + + var renderedComponent = inst._tracker.read(); if (__DEV__) { // We allow auto-mocks to proceed as if they're returning null. if (typeof renderedComponent === 'undefined' && diff --git a/src/renderers/shared/reconciler/ReactDataTracker.js b/src/renderers/shared/reconciler/ReactDataTracker.js new file mode 100644 index 0000000000000..d2972d5bf5ec7 --- /dev/null +++ b/src/renderers/shared/reconciler/ReactDataTracker.js @@ -0,0 +1,172 @@ +/** + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * +* @providesModule ReactDataTracker +*/ +'use strict'; + +// TODO: Using the ES6 Polyfill +// Using expando properties might be a possibility, but I opted away from this because: +// 1. This code isn't production-ready yet anyway, this code is mostly to demo purposes +// 2. New browsers support ES6 maps, so this only has perf ramifications on legacy browsers +// 3. Perhaps most importantly: The data entities are user data objects, meaning that +// they could be frozen, or iterated over, or any number of other edge cases that +// would make adding expando properties a fairly unfriendly thing to do. +var Es6Map = (typeof Map !== 'undefined' ? Map : require('es6-collections').Map); + +var ReactDataTracker = function(dataFunction) { + var tracker = { + _cacheValid: false, + _cachedResult: undefined, + _dataFunction: dataFunction, + read: function() { + ReactDataTracker.startRead(); + ReactDataTracker.endRead(); + if (!tracker._cacheValid) { + ReactDataTracker.startRender(tracker); + tracker._cachedResult = tracker._dataFunction(); + ReactDataTracker.endRender(tracker); + tracker._cacheValid = true; + } + return tracker._cachedResult; + }, + setCallback: function(callback) { + tracker._callback = callback; + }, + destroy: function() { + ReactDataTracker.unmount(tracker); + } + }; + return tracker; +}; + +ReactDataTracker.startRender = function(component) { + ReactDataTracker.currentContext = []; + if (ReactDataTracker.listeners === undefined) { + ReactDataTracker.listeners = new Es6Map(); + } + if (ReactDataTracker.dataSources === undefined) { + ReactDataTracker.dataSources = new Es6Map(); + } + if (!ReactDataTracker.dataSources.has(component)) { + ReactDataTracker.dataSources.set(component, []); + } + }; + +ReactDataTracker.endRender = function(component) { + var oldDataSources = ReactDataTracker.dataSources.get(component); + var newDataSources = ReactDataTracker.currentContext; + var index = 0; + + for (index = 0; index < oldDataSources.length; index++) { + if (newDataSources.indexOf(oldDataSources[index]) === -1) { + var oldListeners = ReactDataTracker.listeners.get(oldDataSources[index]); + oldListeners.splice(oldListeners.indexOf(component), 1); + oldDataSources.splice(index, 1); + index--; + } + } + for (index = 0; index < newDataSources.length; index++) { + if (oldDataSources.indexOf(newDataSources[index]) === -1) { + if (!ReactDataTracker.listeners.has(newDataSources[index])) { + ReactDataTracker.listeners.set(newDataSources[index], []); + } + ReactDataTracker.listeners.get(newDataSources[index]).push(component); + ReactDataTracker.dataSources.get(component).push(newDataSources[index]); + } + } + }; + +ReactDataTracker.startRead = function(entity) { + if (ReactDataTracker.activeReaders === undefined) { + ReactDataTracker.activeReaders = 0; + } + ReactDataTracker.activeReaders++; + }; + +ReactDataTracker.endRead = function(entity) { + if (ReactDataTracker.currentContext !== undefined && ReactDataTracker.currentContext.indexOf(entity) === -1) { + ReactDataTracker.currentContext.push(entity); + } + ReactDataTracker.activeReaders--; + if (ReactDataTracker.activeReaders < 0) { + throw new Error('Number of active readers dropped below zero'); + } + }; + +ReactDataTracker.startWrite = function(entity) { + if (ReactDataTracker.writers === undefined) { + ReactDataTracker.writers = []; + } + if (ReactDataTracker.writers.indexOf(entity) === -1) { + ReactDataTracker.writers.push(entity); + } + if (ReactDataTracker.activeWriters === undefined) { + ReactDataTracker.activeWriters = 0; + } + ReactDataTracker.activeWriters++; + }; + +ReactDataTracker.endWrite = function(entity) { + if (ReactDataTracker.activeWriters === undefined) { + throw new Error('Can not end write without starting write'); + } + if (ReactDataTracker.writers.indexOf(entity) === -1) { + throw new Error('Can not end write without starting write'); + } + ReactDataTracker.activeWriters--; + + if (ReactDataTracker.activeWriters === 0) { + // for each writer that wrote during this batch + var componentsToNotify = []; + for (var writerIndex = 0; writerIndex < ReactDataTracker.writers.length; writerIndex++) { + var writer = ReactDataTracker.writers[writerIndex]; + if (ReactDataTracker.listeners === undefined) { + continue; + } + if (!ReactDataTracker.listeners.has(writer)) { + continue; + } + var listenersList = ReactDataTracker.listeners.get(writer); + for (var index = 0; index < listenersList.length; index++) { + if (componentsToNotify.indexOf(listenersList[index]) === -1) { + componentsToNotify.push(listenersList[index]); + } + } + } + + for (var componentIndex = 0; componentIndex < componentsToNotify.length; componentIndex++) { + var component = componentsToNotify[componentIndex]; + var invokeCallback = component._cacheValid && component._callback !== undefined; + component._cacheValid = false; // Invalidate cache before calling callback + if (invokeCallback) { + component._callback(); + } + } + ReactDataTracker.writers = []; + } + }; + +ReactDataTracker.unmount = function(component) { + var oldDataSources = ReactDataTracker.dataSources.get(component); + if (oldDataSources === undefined) { + return; + } + for (var index = 0; index < oldDataSources.length; index++) { + var entityListeners = ReactDataTracker.listeners.get(oldDataSources[index]); + var entityListenerPosition = entityListeners.indexOf(component); + if (entityListenerPosition > -1) { + entityListeners.splice(entityListeners.indexOf(component), 1); + } else { + throw new Error('Unable to find listener when unmounting component'); + } + } + ReactDataTracker.dataSources.delete(component); + }; + +module.exports = ReactDataTracker; diff --git a/src/shared/vendor/third_party/es6-collections.js b/src/shared/vendor/third_party/es6-collections.js new file mode 100644 index 0000000000000..13d55e4b2a315 --- /dev/null +++ b/src/shared/vendor/third_party/es6-collections.js @@ -0,0 +1,31 @@ +/** + * + * Copyright (C) 2011 by Andrea Giammarchi, @WebReflection + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @providesModule es6-collections + */ + +(function(e){function f(a,c){function b(a){if(!this||this.constructor!==b)return new b(a);this._keys=[];this._values=[];this._itp=[];this.objectOnly=c;a&&v.call(this,a)}c||w(a,"size",{get:x});a.constructor=b;b.prototype=a;return b}function v(a){this.add?a.forEach(this.add,this):a.forEach(function(a){this.set(a[0],a[1])},this)}function d(a){this.has(a)&&(this._keys.splice(b,1),this._values.splice(b,1),this._itp.forEach(function(a){b