Skip to content

Commit

Permalink
Add ReactDataTracker to Addons
Browse files Browse the repository at this point in the history
  • Loading branch information
jimfb committed May 27, 2015
1 parent b687a22 commit 7f71fbd
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/addons/ReactWithAddons.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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__) {
Expand Down
51 changes: 51 additions & 0 deletions src/addons/__tests__/ReactDataTracker-test.js
Original file line number Diff line number Diff line change
@@ -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 <div>{this.props.person.getName()}</div>;
}
}

var container = document.createElement('div');

var person = new Person("jimfb");
React.render(<PersonView person={person} />, container);
expect(container.children[0].innerHTML).toBe('jimfb');
person.setName("Jim");
expect(container.children[0].innerHTML).toBe('Jim');
React.unmountComponentAtNode(container);
});
});
3 changes: 3 additions & 0 deletions src/renderers/dom/client/ReactMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
16 changes: 15 additions & 1 deletion src/renderers/shared/reconciler/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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' &&
Expand Down
172 changes: 172 additions & 0 deletions src/renderers/shared/reconciler/ReactDataTracker.js
Original file line number Diff line number Diff line change
@@ -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;
31 changes: 31 additions & 0 deletions src/shared/vendor/third_party/es6-collections.js
Original file line number Diff line number Diff line change
@@ -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<a[0]&&a[0]--}));return-1<b}function m(a){return this.has(a)?this._values[b]:
void 0}function n(a,c){if(this.objectOnly&&c!==Object(c))throw new TypeError("Invalid value used as weak collection key");if(c!=c||0===c)for(b=a.length;b--&&!y(a[b],c););else b=a.indexOf(c);return-1<b}function p(a){return n.call(this,this._values,a)}function q(a){return n.call(this,this._keys,a)}function r(a,c){this.has(a)?this._values[b]=c:this._values[this._keys.push(a)-1]=c;return this}function t(a){this.has(a)||this._values.push(a);return this}function h(){this._values.length=0}function z(){return k(this._itp,
this._keys)}function l(){return k(this._itp,this._values)}function A(){return k(this._itp,this._keys,this._values)}function B(){return k(this._itp,this._values,this._values)}function k(a,c,b){var g=[0],e=!1;a.push(g);return{next:function(){var f,d=g[0];!e&&d<c.length?(f=b?[c[d],b[d]]:c[d],g[0]++):(e=!0,a.splice(a.indexOf(g),1));return{done:e,value:f}}}}function x(){return this._values.length}function u(a,c){for(var b=this.entries();;){var d=b.next();if(d.done)break;a.call(c,d.value[1],d.value[0],
this)}}var b,w=Object.defineProperty,y=function(a,b){return isNaN(a)?isNaN(b):a===b};"undefined"==typeof WeakMap&&(e.WeakMap=f({"delete":d,clear:h,get:m,has:q,set:r},!0));"undefined"==typeof Map&&(e.Map=f({"delete":d,has:q,get:m,set:r,keys:z,values:l,entries:A,forEach:u,clear:h}));"undefined"==typeof Set&&(e.Set=f({has:p,add:t,"delete":d,clear:h,keys:l,values:l,entries:B,forEach:u}));"undefined"==typeof WeakSet&&(e.WeakSet=f({"delete":d,add:t,clear:h,has:p},!0))})("undefined"!=typeof exports&&"undefined"!=
typeof global?global:window);

0 comments on commit 7f71fbd

Please sign in to comment.