Merge branch '15-dev' into 15-stable

This commit is contained in:
Paul O’Shannessy committed 2016-07-08 15:11:36 -07:00
commit 4b9d48a150
100 files changed
+4168 -1895

No files matched your search

+1 -1
View File
@@ -34,4 +34,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-4]\\|1[0-9]\\|[0-9
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
[version]
^0.26.0
^0.27.0
+28 -2
View File
@@ -1,14 +1,40 @@
## 15.2.1 (July 8, 2016)
### React
- Fix errant warning related to `shouldHaveDebugID`. ([@gaearon](https://github.com/gaearon) in [#7193](https://github.com/facebook/react/pull/7193))
- Better removal of dev-only code, leading to a small reduction in the minified production bundle size. ([@gaearon](https://github.com/gaearon) in [#7188](https://github.com/facebook/react/pull/7188), [#7189](https://github.com/facebook/react/pull/7189))
### React DOM
- Add stack trace to unknown property warning. ([@jimfb](https://github.com/jimfb) in [#7040](https://github.com/facebook/react/pull/7040))
- Fix webcomponents example. ([@jalexanderfox](https://github.com/jalexanderfox) in [#7057](https://github.com/facebook/react/pull/7057))
- Fix `unstable_renderSubtreeIntoContainer` so that context properly updates when linked to state. ([@gaearon](https://github.com/gaearon) in [#7125](https://github.com/facebook/react/pull/7125))
- Improve invariant wording for void elements. ([@starkch](https://github.com/starkch) in [#7066](https://github.com/facebook/react/pull/7066))
- Ensure no errors are thrown due to event handlers in server rendering. ([@rricard](https://github.com/rricard) in [#7127](https://github.com/facebook/react/pull/7127))
- Fix regression resulting in `value`-less submit and reset inputs removing the browser-default text. ([@zpao](https://github.com/zpao) in [#7197](https://github.com/facebook/react/pull/7197))
- Fix regression resulting in empty `name` attribute being added to inputs when not provided. ([@okonet](https://github.com/okonet) in [#7199](https://github.com/facebook/react/pull/7199))
### React Perf Add-on
- Make `ReactPerf.start()` work properly during lifecycle methods. ([@gaearon](https://github.com/gaearon) in [#7208](https://github.com/facebook/react/pull/7208)).
### React CSSTransitionGroup Add-on
- Fix issue resulting in spurious unknown property warnings. ([@batusai513](https://github.com/batusai513) in [#7165](https://github.com/facebook/react/pull/7165))
### React Native Renderer
- Improve error handling in cross-platform touch event handling. ([@yungsters](https://github.com/yungsters) in [#7143](https://github.com/facebook/react/pull/7143))
## 15.2.0 (July 1, 2016)
### React
- Add error codes to production invariants, with links to the view the full error text. ([@keyanzhang](https://github.com/keyanzhang) in [#6948](https://github.com/facebook/react/pull/6948))
- Include component stack information in PropType validation warnings. ([@spicyj](https://github.com/spicyj) in [#6771](https://github.com/facebook/react/pull/6771))
- Include component stack information in PropType validation warnings. ([@troydemonbreun](https://github.com/troydemonbreun) in [#6398](https://github.com/facebook/react/pull/6398), [@spicyj](https://github.com/spicyj) in [#6771](https://github.com/facebook/react/pull/6771))
- Include component stack information in key warnings. ([@keyanzhang](https://github.com/keyanzhang) in [#6799](https://github.com/facebook/react/pull/6799))
- Stop validating props at mount time, only validate at element creation. ([@keyanzhang](https://github.com/keyanzhang) in [#6823](https://github.com/facebook/react/pull/6823))
- Stop validating props at mount time, only validate at element creation. ([@keyanzhang](https://github.com/keyanzhang) in [#6824](https://github.com/facebook/react/pull/6824))
- New invariant providing actionable error in missing instance case. ([@yungsters](https://github.com/yungsters) in [#6990](https://github.com/facebook/react/pull/6990))
- Add `React.PropTypes.symbol` to support ES2015 Symbols as props. ([@puradox](https://github.com/puradox) in [#6377](https://github.com/facebook/react/pull/6377))
- Fix incorrect coercion of ref or key that are undefined in development ([@gaearon](https://github.com/gaearon) in [#6880](https://github.com/facebook/react/pull/6880))
- Fix a false positive when passing other element’s props to cloneElement ([@ericmatthys](https://github.com/ericmatthys) in [#6268](https://github.com/facebook/react/pull/6268))
- Warn if you attempt to define `childContextTypes` on a functional component ([@Aweary](https://github.com/Aweary) in [#6933](https://github.com/facebook/react/pull/6933))
### React DOM
- Add warning for unknown properties on DOM elements. ([@jimfb](https://github.com/jimfb) in [#6800](https://github.com/facebook/react/pull/6800), [@gm758](https://github.com/gm758) in [#7152](https://github.com/facebook/react/pull/7152))
+12 -3
View File
@@ -52,11 +52,17 @@ module.exports = function(grunt) {
grunt.loadNpmTasks(npmTaskName);
});
grunt.registerTask('eslint', require('./grunt/tasks/eslint'));
grunt.registerTask('eslint', function() {
// Use gulp here.
spawnGulp(['eslint'], null, this.async());
});
grunt.registerTask('lint', ['eslint']);
grunt.registerTask('flow', require('./grunt/tasks/flow'));
grunt.registerTask('flow', function() {
// Use gulp here.
spawnGulp(['flow'], null, this.async());
});
grunt.registerTask('delete-build-modules', function() {
// Use gulp here.
@@ -84,7 +90,10 @@ module.exports = function(grunt) {
grunt.registerTask('npm-react-addons:release', npmReactAddonsTasks.buildReleases);
grunt.registerTask('npm-react-addons:pack', npmReactAddonsTasks.packReleases);
grunt.registerTask('version-check', require('./grunt/tasks/version-check'));
grunt.registerTask('version-check', function() {
// Use gulp here.
spawnGulp(['version-check'], null, this.async());
});
grunt.registerTask('build:basic', [
'build-modules',
+1
View File
@@ -65,6 +65,7 @@ The process to build `react.js` is built entirely on top of node.js, using many
#### Prerequisites
* You have `node` installed at v4.0.0+ and `npm` at v2.0.0+.
* You have `gcc` installed or are comfortable installing a compiler if needed. Some of our `npm` dependencies may require a compliation step. On OS X, the Xcode Command Line Tools will cover this. On Ubuntu, `apt-get install build-essential` will install the required packages. Similar commands should work on other Linux distros. Windows will require some additional steps, see the [`node-gyp` installation instructions](https://github.com/nodejs/node-gyp#installation) for details.
* You are familiar with `npm` and know whether or not you need to use `sudo` when installing packages globally.
* You are familiar with `git`.
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fiber Example</title>
<link rel="stylesheet" href="../shared/css/base.css" />
</head>
<body>
<h1>Fiber Example</h1>
<div id="container">
<p>
To install React, follow the instructions on
<a href="https://github.com/facebook/react/">GitHub</a>.
</p>
<p>
If you can see this, React is <strong>not</strong> working right.
If you checked out the source from GitHub make sure to run <code>grunt</code>.
</p>
</div>
<script src="../../build/react.js"></script>
<script src="../../build/react-dom.js"></script>
<script>
function ExampleApplication(props) {
var elapsed = Math.round(props.elapsed / 100);
var seconds = elapsed / 10 + (elapsed % 10 ? '' : '.0' );
var message =
'React has been successfully running for ' + seconds + ' seconds.';
return React.DOM.p(null, message);
}
// Call React.createFactory instead of directly call ExampleApplication({...}) in React.render
var ExampleApplicationFactory = React.createFactory(ExampleApplication);
var start = new Date().getTime();
setInterval(function() {
ReactDOM.render(
ExampleApplicationFactory({elapsed: new Date().getTime() - start}),
document.getElementById('container')
);
}, 50);
</script>
</body>
</html>
+1 -1
View File
@@ -37,7 +37,7 @@
// Define WebComponent
var proto = Object.create(HTMLElement.prototype, {
createdCallback: {
attachedCallback: {
value: function() {
var mountPoint = document.createElement('span');
this.createShadowRoot().appendChild(mountPoint);
-1
View File
@@ -38,7 +38,6 @@ declare module 'UIManager' {
declare function removeSubviewsFromContainerWithID() : void;
declare function replaceExistingNonRootView() : void;
declare function setChildren() : void;
declare function setChildren() : void;
declare function updateView() : void;
}
declare module 'View' { }
-22
View File
@@ -1,22 +0,0 @@
'use strict';
var grunt = require('grunt');
var extension = process.platform === 'win32' ? '.cmd': '';
module.exports = function() {
var done = this.async();
grunt.util.spawn({
cmd: 'node_modules/.bin/eslint' + extension,
args: ['.'],
opts: {stdio: 'inherit'}, // allows colors to passthrough
}, function(err, result, code) {
if (err) {
grunt.log.error('Lint failed');
} else {
grunt.log.ok('Lint passed');
}
done(code === 0);
});
};
-22
View File
@@ -1,22 +0,0 @@
'use strict';
var grunt = require('grunt');
var extension = process.platform === 'win32' ? '.cmd': '';
module.exports = function() {
var done = this.async();
grunt.util.spawn({
cmd: 'node_modules/.bin/flow' + extension,
args: ['check', '.'],
opts: {stdio: 'inherit'},
}, function(err, result, code) {
if (err) {
grunt.log.error('Flow failed');
} else {
grunt.log.ok('Flow passed');
}
done(code === 0);
});
};
-37
View File
@@ -1,37 +0,0 @@
'use strict';
var grunt = require('grunt');
module.exports = function() {
var pkgVersion = grunt.config.data.pkg.version;
var addonsData = grunt.file.readJSON('./packages/react-addons/package.json');
var versions = {
'packages/react/package.json':
grunt.file.readJSON('./packages/react/package.json').version,
'packages/react-dom/package.json':
grunt.file.readJSON('./packages/react-dom/package.json').version,
'packages/react-native-renderer/package.json':
grunt.file.readJSON('./packages/react-native-renderer/package.json').version,
'packages/react-addons/package.json (version)': addonsData.version,
// Get the "version" without the range bit
'packages/react-addons/package.json (react dependency)': addonsData.peerDependencies.react.slice(1),
'src/ReactVersion.js': require('../../src/ReactVersion'),
};
// Return true (ok) or false (failed)
return Object.keys(versions).reduce(function(prev, name) {
var version = versions[name];
var ok = true;
if (version !== pkgVersion) {
grunt.log.error(
'%s version does not match package.json. Expected %s, saw %s.',
name,
pkgVersion,
version
);
ok = false;
}
return prev && ok;
}, true);
};
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright 2013-present, 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.
*/
'use strict';
var path = require('path');
var spawn = require('child_process').spawn;
var extension = process.platform === 'win32' ? '.cmd' : '';
module.exports = function(gulp, plugins) {
var gutil = plugins.util;
return function(done) {
spawn(
process.execPath,
[
path.join('node_modules', '.bin', 'eslint' + extension),
'.',
],
{
// Allow colors to pass through
stdio: 'inherit',
}
).on('close', function(code) {
if (code !== 0) {
gutil.log(
gutil.colors.red(
'Lint failed'
)
);
process.exit(code);
}
gutil.log(
gutil.colors.green(
'Lint passed'
)
);
done();
});
};
};
+50
View File
@@ -0,0 +1,50 @@
/**
* Copyright 2013-present, 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.
*/
'use strict';
var path = require('path');
var spawn = require('child_process').spawn;
var extension = process.platform === 'win32' ? '.cmd' : '';
module.exports = function(gulp, plugins) {
var gutil = plugins.util;
return function(done) {
spawn(
process.execPath,
[
path.join('node_modules', '.bin', 'flow' + extension),
'check',
'.',
],
{
// Allow colors to pass through
stdio: 'inherit',
}
).on('close', function(code) {
if (code !== 0) {
gutil.log(
gutil.colors.red(
'Flow failed'
)
);
process.exit(code);
}
gutil.log(
gutil.colors.green(
'Flow passed'
)
);
done();
});
};
};
+55
View File
@@ -0,0 +1,55 @@
/**
* Copyright 2013-present, 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.
*/
'use strict';
module.exports = function(gulp, plugins) {
var gutil = plugins.util;
return function(done) {
var reactVersion = require('../../package.json').version;
var addonsData = require('../../packages/react-addons/package.json');
var versions = {
'packages/react/package.json':
require('../../packages/react/package.json').version,
'packages/react-dom/package.json':
require('../../packages/react-dom/package.json').version,
'packages/react-native-renderer/package.json':
require('../../packages/react-native-renderer/package.json').version,
'packages/react-addons/package.json (version)': addonsData.version,
'packages/react-addons/package.json (react dependency)':
// Get the "version" without the range bit
addonsData.peerDependencies.react.slice(1),
'src/ReactVersion.js': require('../../src/ReactVersion'),
};
var allVersionsMatch = true;
Object.keys(versions).forEach(function(name) {
var version = versions[name];
if (version !== reactVersion) {
allVersionsMatch = false;
gutil.log(
gutil.colors.red(
'%s version does not match package.json. Expected %s, saw %s.'
),
name,
reactVersion,
version
);
}
});
if (!allVersionsMatch) {
process.exit(1);
}
done();
};
};
+15
View File
@@ -18,6 +18,13 @@ var babelPluginModules = require('fbjs-scripts/babel-6/rewrite-modules');
var extractErrors = require('./scripts/error-codes/gulp-extract-errors');
var devExpressionWithCodes = require('./scripts/error-codes/dev-expression-with-codes');
// Load all of the Gulp plugins.
var plugins = require('gulp-load-plugins')();
function getTask(name) {
return require(`./gulp/tasks/${name}`)(gulp, plugins);
}
var paths = {
react: {
src: [
@@ -60,6 +67,14 @@ var babelOpts = {
],
};
gulp.task('eslint', getTask('eslint'));
gulp.task('lint', ['eslint']);
gulp.task('flow', getTask('flow'));
gulp.task('version-check', getTask('version-check'));
gulp.task('react:clean', function() {
return del([paths.react.lib]);
});
+1112 -617
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -43,7 +43,7 @@
"eslint-plugin-react-internal": "file:eslint-rules",
"fbjs": "^0.8.1",
"fbjs-scripts": "^0.6.0",
"flow-bin": "^0.26.0",
"flow-bin": "^0.27.0",
"glob": "^6.0.1",
"grunt": "^0.4.5",
"grunt-cli": "^0.1.13",
@@ -53,6 +53,7 @@
"gulp": "^3.9.0",
"gulp-babel": "^6.0.0",
"gulp-flatten": "^0.2.0",
"gulp-load-plugins": "^1.2.4",
"gulp-util": "^3.0.7",
"gzip-js": "~0.3.2",
"jest": "^12.1.1",
@@ -78,7 +79,8 @@
"linc": "git diff --name-only --diff-filter=ACMRTUB `git merge-base HEAD master` | grep '\\.js$' | xargs eslint --",
"lint": "grunt lint",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json",
"test": "jest"
"test": "jest",
"flow": "flow"
},
"jest": {
"modulePathIgnorePatterns": [
+3 -1
View File
@@ -135,5 +135,7 @@
"133": "Touch object is missing identifier",
"134": "Touch data should have been recorded on start",
"135": "Cannot find single active touch",
"136": "Attempted to update component `%s` that has already been unmounted (or failed to mount)."
"136": "Attempted to update component `%s` that has already been unmounted (or failed to mount).",
"137": "%s is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.%s",
"138": "Touch object is missing identifier."
}
@@ -12,13 +12,13 @@
'use strict';
var React = require('React');
var ReactDOM = require('ReactDOM');
var ReactTestUtils = require('ReactTestUtils');
var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer');
describe('renderSubtreeIntoContainer', function() {
it('should pass context when rendering subtree elsewhere', function() {
var portal = document.createElement('div');
var Component = React.createClass({
@@ -92,4 +92,107 @@ describe('renderSubtreeIntoContainer', function() {
},
});
});
it('should update context if it changes due to setState', function() {
var container = document.createElement('div');
document.body.appendChild(container);
var portal = document.createElement('div');
var Component = React.createClass({
contextTypes: {
foo: React.PropTypes.string.isRequired,
getFoo: React.PropTypes.func.isRequired,
},
render: function() {
return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
},
});
var Parent = React.createClass({
childContextTypes: {
foo: React.PropTypes.string.isRequired,
getFoo: React.PropTypes.func.isRequired,
},
getChildContext: function() {
return {
foo: this.state.bar,
getFoo: () => this.state.bar,
};
},
getInitialState: function() {
return {
bar: 'initial',
};
},
render: function() {
return null;
},
componentDidMount: function() {
renderSubtreeIntoContainer(this, <Component />, portal);
},
componentDidUpdate() {
renderSubtreeIntoContainer(this, <Component />, portal);
},
});
var instance = ReactDOM.render(<Parent />, container);
expect(portal.firstChild.innerHTML).toBe('initial-initial');
instance.setState({bar: 'changed'});
expect(portal.firstChild.innerHTML).toBe('changed-changed');
});
it('should update context if it changes due to re-render', function() {
var container = document.createElement('div');
document.body.appendChild(container);
var portal = document.createElement('div');
var Component = React.createClass({
contextTypes: {
foo: React.PropTypes.string.isRequired,
getFoo: React.PropTypes.func.isRequired,
},
render: function() {
return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
},
});
var Parent = React.createClass({
childContextTypes: {
foo: React.PropTypes.string.isRequired,
getFoo: React.PropTypes.func.isRequired,
},
getChildContext: function() {
return {
foo: this.props.bar,
getFoo: () => this.props.bar,
};
},
render: function() {
return null;
},
componentDidMount: function() {
renderSubtreeIntoContainer(this, <Component />, portal);
},
componentDidUpdate() {
renderSubtreeIntoContainer(this, <Component />, portal);
},
});
ReactDOM.render(<Parent bar="initial" />, container);
expect(portal.firstChild.innerHTML).toBe('initial-initial');
ReactDOM.render(<Parent bar="changed" />, container);
expect(portal.firstChild.innerHTML).toBe('changed-changed');
});
});
@@ -266,6 +266,7 @@ var ReactTransitionGroup = React.createClass({
delete props.childFactory;
delete props.transitionLeaveTimeout;
delete props.transitionEnterTimeout;
delete props.transitionAppearTimeout;
delete props.component;
return React.createElement(
@@ -11,7 +11,6 @@
'use strict';
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
var invariant = require('invariant');
@@ -72,10 +71,13 @@ function checkReactTypeSpec(typeSpecs, values, location, componentName, element,
var componentStackInfo = '';
if (debugID !== null) {
componentStackInfo = ReactComponentTreeDevtool.getStackAddendumByID(debugID);
} else if (element !== null) {
componentStackInfo = ReactComponentTreeDevtool.getCurrentStackAddendum(element);
if (__DEV__) {
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
if (debugID !== null) {
componentStackInfo = ReactComponentTreeDevtool.getStackAddendumByID(debugID);
} else if (element !== null) {
componentStackInfo = ReactComponentTreeDevtool.getCurrentStackAddendum(element);
}
}
warning(
@@ -32,10 +32,6 @@ function updateTree(id, update) {
isMounted: false,
updateCount: 0,
};
// TODO: We need to do this awkward dance because TopLevelWrapper "never
// gets mounted" but its display name gets set in instantiateReactComponent
// before its debug ID is set to 0.
unmountedIDs[id] = true;
}
update(tree[id]);
}
@@ -148,7 +144,6 @@ var ReactComponentTreeDevtool = {
onMountComponent(id) {
updateTree(id, item => item.isMounted = true);
delete unmountedIDs[id];
},
onMountRootComponent(id) {
@@ -13,8 +13,9 @@
var warning = require('warning');
function warnTDZ(publicInstance, callerName) {
function warnNoop(publicInstance, callerName) {
if (__DEV__) {
var constructor = publicInstance.constructor;
warning(
false,
'%s(...): Can only update a mounted or mounting component. ' +
@@ -22,7 +23,7 @@ function warnTDZ(publicInstance, callerName) {
'This is a no-op. Please check the code for the %s component.',
callerName,
callerName,
publicInstance.constructor && publicInstance.constructor.displayName || ''
constructor && (constructor.displayName || constructor.name) || 'ReactClass'
);
}
}
@@ -67,7 +68,7 @@ var ReactNoopUpdateQueue = {
* @internal
*/
enqueueForceUpdate: function(publicInstance) {
warnTDZ(publicInstance, 'forceUpdate');
warnNoop(publicInstance, 'forceUpdate');
},
/**
@@ -82,7 +83,7 @@ var ReactNoopUpdateQueue = {
* @internal
*/
enqueueReplaceState: function(publicInstance, completeState) {
warnTDZ(publicInstance, 'replaceState');
warnNoop(publicInstance, 'replaceState');
},
/**
@@ -96,7 +97,7 @@ var ReactNoopUpdateQueue = {
* @internal
*/
enqueueSetState: function(publicInstance, partialState) {
warnTDZ(publicInstance, 'setState');
warnNoop(publicInstance, 'setState');
},
};
@@ -362,8 +362,6 @@ describe 'ReactCoffeeScriptClass', ->
test Inner(name: 'foo'), 'DIV', 'foo'
expect(-> instance.replaceState {}).toThrow()
expect(-> instance.isMounted()).toThrow()
expect(-> instance.setProps name: 'bar').toThrow()
expect(-> instance.replaceProps name: 'bar').toThrow()
expect(console.error.calls.count()).toBe 2
expect(console.error.calls.argsFor(0)[0]).toContain(
'replaceState(...) is deprecated in plain JavaScript React classes'
@@ -402,8 +402,6 @@ describe('ReactES6Class', function() {
var instance = test(<Inner name="foo" />, 'DIV', 'foo');
expect(() => instance.replaceState({})).toThrow();
expect(() => instance.isMounted()).toThrow();
expect(() => instance.setProps({name: 'bar'})).toThrow();
expect(() => instance.replaceProps({name: 'bar'})).toThrow();
expect(console.error.calls.count()).toBe(2);
expect(console.error.calls.argsFor(0)[0]).toContain(
'replaceState(...) is deprecated in plain JavaScript React classes'
@@ -489,8 +489,6 @@ describe('ReactTypeScriptClass', function() {
);
expect(() => instance.replaceState({})).toThrow();
expect(() => instance.isMounted()).toThrow();
expect(() => instance.setProps({ name: 'bar' })).toThrow();
expect(() => instance.replaceProps({ name: 'bar' })).toThrow();
expect((<any>console.error).calls.count()).toBe(2);
expect((<any>console.error).calls.argsFor(0)[0]).toContain(
'replaceState(...) is deprecated in plain JavaScript React classes'
+15 -19
View File
@@ -20,6 +20,7 @@ var ReactDOMContainerInfo = require('ReactDOMContainerInfo');
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var ReactElement = require('ReactElement');
var ReactFeatureFlags = require('ReactFeatureFlags');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactInstrumentation = require('ReactInstrumentation');
var ReactMarkupChecksum = require('ReactMarkupChecksum');
var ReactReconciler = require('ReactReconciler');
@@ -287,10 +288,11 @@ var ReactMount = {
_updateRootComponent: function(
prevComponent,
nextElement,
nextContext,
container,
callback) {
ReactMount.scrollMonitor(container, function() {
ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);
ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement, nextContext);
if (callback) {
ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
}
@@ -313,10 +315,6 @@ var ReactMount = {
shouldReuseMarkup,
context
) {
if (__DEV__) {
ReactInstrumentation.debugTool.onBeginFlush();
}
// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case.
@@ -340,13 +338,7 @@ var ReactMount = {
);
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
var componentInstance = instantiateReactComponent(nextElement);
if (__DEV__) {
// Mute future events from the top level wrapper.
// It is an implementation detail that devtools should not know about.
componentInstance._debugID = 0;
}
var componentInstance = instantiateReactComponent(nextElement, false);
// The initial render is synchronous but any updates that happen during
// rendering, in componentWillMount or componentDidMount, will be batched
@@ -368,7 +360,6 @@ var ReactMount = {
ReactInstrumentation.debugTool.onMountRootComponent(
componentInstance._renderedComponent._debugID
);
ReactInstrumentation.debugTool.onEndFlush();
}
return componentInstance;
@@ -389,7 +380,7 @@ var ReactMount = {
*/
renderSubtreeIntoContainer: function(parentComponent, nextElement, container, callback) {
invariant(
parentComponent != null && parentComponent._reactInternalInstance != null,
parentComponent != null && ReactInstanceMap.has(parentComponent),
'parentComponent must be a valid React Component'
);
return ReactMount._renderSubtreeIntoContainer(
@@ -440,6 +431,14 @@ var ReactMount = {
nextElement
);
var nextContext;
if (parentComponent) {
var parentInst = ReactInstanceMap.get(parentComponent);
nextContext = parentInst._processChildContext(parentInst._context);
} else {
nextContext = emptyObject;
}
var prevComponent = getTopLevelWrapperInContainer(container);
if (prevComponent) {
@@ -453,6 +452,7 @@ var ReactMount = {
ReactMount._updateRootComponent(
prevComponent,
nextWrappedElement,
nextContext,
container,
updatedCallback
);
@@ -501,11 +501,7 @@ var ReactMount = {
nextWrappedElement,
container,
shouldReuseMarkup,
parentComponent != null ?
parentComponent._reactInternalInstance._processChildContext(
parentComponent._reactInternalInstance._context
) :
emptyObject
nextContext
)._renderedComponent.getPublicInstance();
if (callback) {
callback.call(component);
@@ -15,7 +15,9 @@ var CallbackQueue = require('CallbackQueue');
var PooledClass = require('PooledClass');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactInputSelection = require('ReactInputSelection');
var ReactInstrumentation = require('ReactInstrumentation');
var Transaction = require('Transaction');
var ReactUpdateQueue = require('ReactUpdateQueue');
/**
@@ -90,6 +92,13 @@ var TRANSACTION_WRAPPERS = [
ON_DOM_READY_QUEUEING,
];
if (__DEV__) {
TRANSACTION_WRAPPERS.push({
initialize: ReactInstrumentation.debugTool.onBeginFlush,
close: ReactInstrumentation.debugTool.onEndFlush,
});
}
/**
* Currently:
* - The order that these are listed in the transaction is critical:
@@ -104,7 +113,7 @@ var TRANSACTION_WRAPPERS = [
*
* @class ReactReconcileTransaction
*/
function ReactReconcileTransaction(useCreateElement) {
function ReactReconcileTransaction(useCreateElement: boolean) {
this.reinitializeTransaction();
// Only server-side rendering really needs this option (see
// `ReactServerRendering`), but server-side uses
@@ -135,6 +144,13 @@ var Mixin = {
return this.reactMountReady;
},
/**
* @return {object} The queue to collect React async events.
*/
getUpdateQueue: function() {
return ReactUpdateQueue;
},
/**
* Save current transaction state -- if the return value from this method is
* passed to `rollback`, the transaction will be reset to that state.
@@ -22,7 +22,6 @@ var warning = require('warning');
var didWarnValueLink = false;
var didWarnCheckedLink = false;
var didWarnValueNull = false;
var didWarnValueDefaultValue = false;
var didWarnCheckedDefaultChecked = false;
var didWarnControlledToUncontrolled = false;
@@ -35,19 +34,6 @@ function forceUpdateIfMounted() {
}
}
function warnIfValueIsNull(props) {
if (props != null && props.value === null && !didWarnValueNull) {
warning(
false,
'`value` prop on `input` should not be null. ' +
'Consider using the empty string to clear the component or `undefined` ' +
'for uncontrolled components.'
);
didWarnValueNull = true;
}
}
function isControlled(props) {
var usesChecked = props.type === 'checkbox' || props.type === 'radio';
return usesChecked ? props.checked !== undefined : props.value !== undefined;
@@ -149,7 +135,6 @@ var ReactDOMInput = {
);
didWarnValueDefaultValue = true;
}
warnIfValueIsNull(props);
}
var defaultValue = props.defaultValue;
@@ -169,8 +154,6 @@ var ReactDOMInput = {
var props = inst._currentElement.props;
if (__DEV__) {
warnIfValueIsNull(props);
var controlled = isControlled(props);
var owner = inst._currentElement._owner;
@@ -233,10 +216,20 @@ var ReactDOMInput = {
},
postMountWrapper: function(inst) {
var props = inst._currentElement.props;
// This is in postMount because we need access to the DOM node, which is not
// available until after the component has mounted.
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
node.value = node.value; // Detach value from defaultValue
// Detach value from defaultValue. We won't do anything if we're working on
// submit or reset inputs as those values & defaultValues are linked. They
// are not resetable nodes so this operation doesn't matter and actually
// removes browser-default values (eg "Submit Query") when no value is
// provided.
if (props.type !== 'submit' && props.type !== 'reset') {
node.value = node.value;
}
// Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
// this is needed to work around a chrome bug where setting defaultChecked
@@ -244,10 +237,14 @@ var ReactDOMInput = {
// Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
// We need to temporarily unset name to avoid disrupting radio button groups.
var name = node.name;
node.name = undefined;
if (name !== '') {
node.name = '';
}
node.defaultChecked = !node.defaultChecked;
node.defaultChecked = !node.defaultChecked;
node.name = name;
if (name !== '') {
node.name = name;
}
},
};
@@ -19,7 +19,6 @@ var ReactUpdates = require('ReactUpdates');
var warning = require('warning');
var didWarnValueLink = false;
var didWarnValueNull = false;
var didWarnValueDefaultValue = false;
function updateOptionsIfPendingUpdateAndMounted() {
@@ -45,19 +44,6 @@ function getDeclarationErrorAddendum(owner) {
return '';
}
function warnIfValueIsNull(props) {
if (props != null && props.value === null && !didWarnValueNull) {
warning(
false,
'`value` prop on `select` should not be null. ' +
'Consider using the empty string to clear the component or `undefined` ' +
'for uncontrolled components.'
);
didWarnValueNull = true;
}
}
var valuePropNames = ['value', 'defaultValue'];
/**
@@ -168,7 +154,6 @@ var ReactDOMSelect = {
mountWrapper: function(inst, props) {
if (__DEV__) {
checkSelectPropTypes(inst, props);
warnIfValueIsNull(props);
}
var value = LinkedValueUtils.getValue(props);
@@ -205,9 +190,6 @@ var ReactDOMSelect = {
postUpdateWrapper: function(inst) {
var props = inst._currentElement.props;
if (__DEV__) {
warnIfValueIsNull(props);
}
// After the initial mount, we control selected-ness manually so don't pass
// this value down
@@ -20,7 +20,6 @@ var invariant = require('invariant');
var warning = require('warning');
var didWarnValueLink = false;
var didWarnValueNull = false;
var didWarnValDefaultVal = false;
function forceUpdateIfMounted() {
@@ -30,19 +29,6 @@ function forceUpdateIfMounted() {
}
}
function warnIfValueIsNull(props) {
if (props != null && props.value === null && !didWarnValueNull) {
warning(
false,
'`value` prop on `textarea` should not be null. ' +
'Consider using the empty string to clear the component or `undefined` ' +
'for uncontrolled components.'
);
didWarnValueNull = true;
}
}
/**
* Implements a <textarea> host component that allows setting `value`, and
* `defaultValue`. This differs from the traditional DOM API because value is
@@ -109,7 +95,6 @@ var ReactDOMTextarea = {
);
didWarnValDefaultVal = true;
}
warnIfValueIsNull(props);
}
@@ -159,10 +144,6 @@ var ReactDOMTextarea = {
updateWrapper: function(inst) {
var props = inst._currentElement.props;
if (__DEV__) {
warnIfValueIsNull(props);
}
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
var value = LinkedValueUtils.getValue(props);
if (value != null) {
@@ -102,6 +102,35 @@ describe('ReactDOMInput', function() {
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
});
it('should render name attribute if it is supplied', function() {
var container = document.createElement('div');
var node = ReactDOM.render(<input type="text" name="name" />, container);
expect(node.name).toBe('name');
expect(container.firstChild.getAttribute('name')).toBe('name');
});
it('should render name attribute if it is supplied for SSR', function() {
var element = <input type="text" name="name" />;
var markup = ReactDOMServer.renderToString(element);
var div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('name')).toBe('name');
});
it('should not render name attribute if it is not supplied', function() {
var container = document.createElement('div');
ReactDOM.render(<input type="text" />, container);
expect(container.firstChild.getAttribute('name')).toBe(null);
});
it('should not render name attribute if it is not supplied for SSR', function() {
var element = <input type="text" />;
var markup = ReactDOMServer.renderToString(element);
var div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('name')).toBe(null);
});
it('should display "foobar" for `defaultValue` of `objToString`', function() {
var objToString = {
toString: function() {
@@ -672,7 +701,7 @@ describe('ReactDOMInput', function() {
value="value"
defaultChecked={true}
checked={false}
onChange={() => null}
onChange={() => null}
/>, container);
expect(console.error.calls.count()).toBe(0);
});
@@ -718,10 +747,8 @@ describe('ReactDOMInput', function() {
'set type',
'set value',
'set value',
'set name',
'set checked',
'set checked',
'set name',
]);
});
+92
View File
@@ -0,0 +1,92 @@
/**
* Copyright 2013-present, 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 ReactDOMFiber
* @flow
*/
'use strict';
import type { HostChildren } from 'ReactFiberReconciler';
var ReactFiberReconciler = require('ReactFiberReconciler');
type DOMContainerElement = Element & { _reactRootContainer: Object };
type Container = Element;
type Props = { };
type Instance = Element;
function recursivelyAppendChildren(parent : Element, child : HostChildren<Instance>) {
if (!child) {
return;
}
/* $FlowFixMe: Element should have this property. */
if (child.nodeType === 1) {
/* $FlowFixMe: Refinement issue. I don't know how to express different. */
parent.appendChild(child);
} else {
/* As a result of the refinement issue this type isn't known. */
let node : any = child;
do {
recursivelyAppendChildren(parent, node.output);
} while (node = node.sibling);
}
}
var DOMRenderer = ReactFiberReconciler({
updateContainer(container : Container, children : HostChildren<Instance>) : void {
container.innerHTML = '';
recursivelyAppendChildren(container, children);
},
createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
const domElement = document.createElement(type);
recursivelyAppendChildren(domElement, children);
if (typeof props.children === 'string') {
domElement.textContent = props.children;
}
return domElement;
},
prepareUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : boolean {
return true;
},
commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
domElement.innerHTML = '';
recursivelyAppendChildren(domElement, children);
if (typeof newProps.children === 'string') {
domElement.textContent = newProps.children;
}
},
deleteInstance(instance : Instance) : void {
// Noop
},
scheduleHighPriCallback: window.requestAnimationFrame,
scheduleLowPriCallback: window.requestIdleCallback,
});
var ReactDOM = {
render(element : ReactElement<any>, container : DOMContainerElement) {
if (!container._reactRootContainer) {
container._reactRootContainer = DOMRenderer.mountContainer(element, container);
} else {
DOMRenderer.updateContainer(element, container._reactRootContainer);
}
},
};
module.exports = ReactDOM;
@@ -37,10 +37,7 @@ function renderToStringImpl(element, makeStaticMarkup) {
transaction = ReactServerRenderingTransaction.getPooled(makeStaticMarkup);
return transaction.perform(function() {
if (__DEV__) {
ReactInstrumentation.debugTool.onBeginFlush();
}
var componentInstance = instantiateReactComponent(element);
var componentInstance = instantiateReactComponent(element, true);
var markup = ReactReconciler.mountComponent(
componentInstance,
transaction,
@@ -52,7 +49,6 @@ function renderToStringImpl(element, makeStaticMarkup) {
ReactInstrumentation.debugTool.onUnmountComponent(
componentInstance._debugID
);
ReactInstrumentation.debugTool.onEndFlush();
}
if (!makeStaticMarkup) {
markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
@@ -13,6 +13,8 @@
var PooledClass = require('PooledClass');
var Transaction = require('Transaction');
var ReactInstrumentation = require('ReactInstrumentation');
var ReactServerUpdateQueue = require('ReactServerUpdateQueue');
/**
@@ -22,6 +24,13 @@ var Transaction = require('Transaction');
*/
var TRANSACTION_WRAPPERS = [];
if (__DEV__) {
TRANSACTION_WRAPPERS.push({
initialize: ReactInstrumentation.debugTool.onBeginFlush,
close: ReactInstrumentation.debugTool.onEndFlush,
});
}
var noopCallbackQueue = {
enqueue: function() {},
};
@@ -34,6 +43,7 @@ function ReactServerRenderingTransaction(renderToStaticMarkup) {
this.reinitializeTransaction();
this.renderToStaticMarkup = renderToStaticMarkup;
this.useCreateElement = false;
this.updateQueue = new ReactServerUpdateQueue(this);
}
var Mixin = {
@@ -54,6 +64,13 @@ var Mixin = {
return noopCallbackQueue;
},
/**
* @return {object} The queue to collect React async events.
*/
getUpdateQueue: function() {
return this.updateQueue;
},
/**
* `PooledClass` looks for this, and will invoke this before allowing this
* instance to be reused.
@@ -0,0 +1,132 @@
/**
* Copyright 2015-present, 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 ReactServerUpdateQueue
* @flow
*/
'use strict';
var ReactUpdateQueue = require('ReactUpdateQueue');
var Transaction = require('Transaction');
var warning = require('warning');
function warnNoop(publicInstance: ReactComponent<any, any, any>, callerName: string) {
if (__DEV__) {
var constructor = publicInstance.constructor;
warning(
false,
'%s(...): Can only update a mounting component. ' +
'This usually means you called %s() outside componentWillMount() on the server. ' +
'This is a no-op. Please check the code for the %s component.',
callerName,
callerName,
constructor && (constructor.displayName || constructor.name) || 'ReactClass'
);
}
}
/**
* This is the update queue used for server rendering.
* It delegates to ReactUpdateQueue while server rendering is in progress and
* switches to ReactNoopUpdateQueue after the transaction has completed.
* @class ReactServerUpdateQueue
* @param {Transaction} transaction
*/
class ReactServerUpdateQueue {
/* :: transaction: Transaction; */
constructor(transaction: Transaction) {
this.transaction = transaction;
}
/**
* Checks whether or not this composite component is mounted.
* @param {ReactClass} publicInstance The instance we want to test.
* @return {boolean} True if mounted, false otherwise.
* @protected
* @final
*/
isMounted(publicInstance: ReactComponent<any, any, any>): boolean {
return false;
}
/**
* Enqueue a callback that will be executed after all the pending updates
* have processed.
*
* @param {ReactClass} publicInstance The instance to use as `this` context.
* @param {?function} callback Called after state is updated.
* @internal
*/
enqueueCallback(publicInstance: ReactComponent<any, any, any>, callback?: Function, callerName?: string) {
if (this.transaction.isInTransaction()) {
ReactUpdateQueue.enqueueCallback(publicInstance, callback, callerName);
}
}
/**
* Forces an update. This should only be invoked when it is known with
* certainty that we are **not** in a DOM transaction.
*
* You may want to call this when you know that some deeper aspect of the
* component's state has changed but `setState` was not called.
*
* This will not invoke `shouldComponentUpdate`, but it will invoke
* `componentWillUpdate` and `componentDidUpdate`.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @internal
*/
enqueueForceUpdate(publicInstance: ReactComponent<any, any, any>) {
if (this.transaction.isInTransaction()) {
ReactUpdateQueue.enqueueForceUpdate(publicInstance);
} else {
warnNoop(publicInstance, 'forceUpdate');
}
}
/**
* Replaces all of the state. Always use this or `setState` to mutate state.
* You should treat `this.state` as immutable.
*
* There is no guarantee that `this.state` will be immediately updated, so
* accessing `this.state` after calling this method may return the old value.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object|function} completeState Next state.
* @internal
*/
enqueueReplaceState(publicInstance: ReactComponent<any, any, any>, completeState: Object|Function) {
if (this.transaction.isInTransaction()) {
ReactUpdateQueue.enqueueReplaceState(publicInstance, completeState);
} else {
warnNoop(publicInstance, 'replaceState');
}
}
/**
* Sets a subset of the state. This only exists because _pendingState is
* internal. This provides a merging strategy that is not available to deep
* properties which is confusing. TODO: Expose pendingState or don't use it
* during the merge.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object|function} partialState Next partial state to be merged with state.
* @internal
*/
enqueueSetState(publicInstance: ReactComponent<any, any, any>, partialState: Object|Function) {
if (this.transaction.isInTransaction()) {
ReactUpdateQueue.enqueueSetState(publicInstance, partialState);
} else {
warnNoop(publicInstance, 'setState');
}
}
}
module.exports = ReactServerUpdateQueue;
@@ -400,4 +400,82 @@ describe('ReactServerRendering', function() {
expect(markup.indexOf('hello, world') >= 0).toBe(true);
});
});
it('warns with a no-op when an async setState is triggered', function() {
class Foo extends React.Component {
componentWillMount() {
this.setState({text: 'hello'});
setTimeout(() => {
this.setState({text: 'error'});
});
}
render() {
return <div onClick={() => {}}>{this.state.text}</div>;
}
}
spyOn(console, 'error');
ReactServerRendering.renderToString(<Foo />);
jest.runOnlyPendingTimers();
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.mostRecent().args[0]).toBe(
'Warning: setState(...): Can only update a mounting component.' +
' This usually means you called setState() outside componentWillMount() on the server.' +
' This is a no-op. Please check the code for the Foo component.'
);
var markup = ReactServerRendering.renderToStaticMarkup(<Foo />);
expect(markup).toBe('<div>hello</div>');
});
it('warns with a no-op when an async replaceState is triggered', function() {
var Bar = React.createClass({
componentWillMount: function() {
this.replaceState({text: 'hello'});
setTimeout(() => {
this.replaceState({text: 'error'});
});
},
render: function() {
return <div onClick={() => {}}>{this.state.text}</div>;
},
});
spyOn(console, 'error');
ReactServerRendering.renderToString(<Bar />);
jest.runOnlyPendingTimers();
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.mostRecent().args[0]).toBe(
'Warning: replaceState(...): Can only update a mounting component. ' +
'This usually means you called replaceState() outside componentWillMount() on the server. ' +
'This is a no-op. Please check the code for the Bar component.'
);
var markup = ReactServerRendering.renderToStaticMarkup(<Bar />);
expect(markup).toBe('<div>hello</div>');
});
it('warns with a no-op when an async forceUpdate is triggered', function() {
var Baz = React.createClass({
componentWillMount: function() {
this.forceUpdate();
setTimeout(() => {
this.forceUpdate();
});
},
render: function() {
return <div onClick={() => {}}></div>;
},
});
spyOn(console, 'error');
ReactServerRendering.renderToString(<Baz />);
jest.runOnlyPendingTimers();
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.mostRecent().args[0]).toBe(
'Warning: forceUpdate(...): Can only update a mounting component. ' +
'This usually means you called forceUpdate() outside componentWillMount() on the server. ' +
'This is a no-op. Please check the code for the Baz component.'
);
var markup = ReactServerRendering.renderToStaticMarkup(<Baz />);
expect(markup).toBe('<div></div>');
});
});
-13
View File
@@ -23,7 +23,6 @@ var DOMPropertyInjection = {
* specifies how the associated DOM property should be accessed or rendered.
*/
MUST_USE_PROPERTY: 0x1,
HAS_SIDE_EFFECTS: 0x2,
HAS_BOOLEAN_VALUE: 0x4,
HAS_NUMERIC_VALUE: 0x8,
HAS_POSITIVE_NUMERIC_VALUE: 0x10 | 0x8,
@@ -91,7 +90,6 @@ var DOMPropertyInjection = {
mutationMethod: null,
mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY),
hasSideEffects: checkMask(propConfig, Injection.HAS_SIDE_EFFECTS),
hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE),
hasNumericValue: checkMask(propConfig, Injection.HAS_NUMERIC_VALUE),
hasPositiveNumericValue:
@@ -99,12 +97,6 @@ var DOMPropertyInjection = {
hasOverloadedBooleanValue:
checkMask(propConfig, Injection.HAS_OVERLOADED_BOOLEAN_VALUE),
};
invariant(
propertyInfo.mustUseProperty || !propertyInfo.hasSideEffects,
'DOMProperty: Properties that have side effects must use property: %s',
propName
);
invariant(
propertyInfo.hasBooleanValue + propertyInfo.hasNumericValue +
propertyInfo.hasOverloadedBooleanValue <= 1,
@@ -183,11 +175,6 @@ var DOMProperty = {
* initial render.
* mustUseProperty:
* Whether the property must be accessed and mutated as an object property.
* hasSideEffects:
* Whether or not setting a value causes side effects such as triggering
* resources to be loaded or text selection changes. If true, we read from
* the DOM before updating to ensure that the value is only set if it has
* changed.
* hasBooleanValue:
* Whether the property should be removed when set to a falsey value.
* hasNumericValue:
@@ -145,15 +145,9 @@ var DOMPropertyOperations = {
this.deleteValueForProperty(node, name);
return;
} else if (propertyInfo.mustUseProperty) {
var propName = propertyInfo.propertyName;
// Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
// property type before comparing; only `value` does and is string.
if (!propertyInfo.hasSideEffects ||
('' + node[propName]) !== ('' + value)) {
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
node[propName] = value;
}
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
node[propertyInfo.propertyName] = value;
} else {
var attributeName = propertyInfo.attributeName;
var namespace = propertyInfo.attributeNamespace;
@@ -240,13 +234,9 @@ var DOMPropertyOperations = {
} else if (propertyInfo.mustUseProperty) {
var propName = propertyInfo.propertyName;
if (propertyInfo.hasBooleanValue) {
// No HAS_SIDE_EFFECTS logic here, only `value` has it and is string.
node[propName] = false;
} else {
if (!propertyInfo.hasSideEffects ||
('' + node[propName]) !== '') {
node[propName] = '';
}
node[propName] = '';
}
} else {
node.removeAttribute(propertyInfo.attributeName);
-127
View File
@@ -16,137 +16,10 @@ var ExecutionEnvironment = require('ExecutionEnvironment');
var createNodesFromMarkup = require('createNodesFromMarkup');
var emptyFunction = require('emptyFunction');
var getMarkupWrap = require('getMarkupWrap');
var invariant = require('invariant');
var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/;
var RESULT_INDEX_ATTR = 'data-danger-index';
/**
* Extracts the `nodeName` from a string of markup.
*
* NOTE: Extracting the `nodeName` does not require a regular expression match
* because we make assumptions about React-generated markup (i.e. there are no
* spaces surrounding the opening tag and there is at least one attribute).
*
* @param {string} markup String of markup.
* @return {string} Node name of the supplied markup.
* @see http://jsperf.com/extract-nodename
*/
function getNodeName(markup) {
return markup.substring(1, markup.indexOf(' '));
}
var Danger = {
/**
* Renders markup into an array of nodes. The markup is expected to render
* into a list of root nodes. Also, the length of `resultList` and
* `markupList` should be the same.
*
* @param {array<string>} markupList List of markup strings to render.
* @return {array<DOMElement>} List of rendered nodes.
* @internal
*/
dangerouslyRenderMarkup: function(markupList) {
invariant(
ExecutionEnvironment.canUseDOM,
'dangerouslyRenderMarkup(...): Cannot render markup in a worker ' +
'thread. Make sure `window` and `document` are available globally ' +
'before requiring React when unit testing or use ' +
'ReactDOMServer.renderToString for server rendering.'
);
var nodeName;
var markupByNodeName = {};
// Group markup by `nodeName` if a wrap is necessary, else by '*'.
for (var i = 0; i < markupList.length; i++) {
invariant(
markupList[i],
'dangerouslyRenderMarkup(...): Missing markup.'
);
nodeName = getNodeName(markupList[i]);
nodeName = getMarkupWrap(nodeName) ? nodeName : '*';
markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
markupByNodeName[nodeName][i] = markupList[i];
}
var resultList = [];
var resultListAssignmentCount = 0;
for (nodeName in markupByNodeName) {
if (!markupByNodeName.hasOwnProperty(nodeName)) {
continue;
}
var markupListByNodeName = markupByNodeName[nodeName];
// This for-in loop skips the holes of the sparse array. The order of
// iteration should follow the order of assignment, which happens to match
// numerical index order, but we don't rely on that.
var resultIndex;
for (resultIndex in markupListByNodeName) {
if (markupListByNodeName.hasOwnProperty(resultIndex)) {
var markup = markupListByNodeName[resultIndex];
// Push the requested markup with an additional RESULT_INDEX_ATTR
// attribute. If the markup does not start with a < character, it
// will be discarded below (with an appropriate console.error).
markupListByNodeName[resultIndex] = markup.replace(
OPEN_TAG_NAME_EXP,
// This index will be parsed back out below.
'$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" '
);
}
}
// Render each group of markup with similar wrapping `nodeName`.
var renderNodes = createNodesFromMarkup(
markupListByNodeName.join(''),
emptyFunction // Do nothing special with <script> tags.
);
for (var j = 0; j < renderNodes.length; ++j) {
var renderNode = renderNodes[j];
if (renderNode.hasAttribute &&
renderNode.hasAttribute(RESULT_INDEX_ATTR)) {
resultIndex = +renderNode.getAttribute(RESULT_INDEX_ATTR);
renderNode.removeAttribute(RESULT_INDEX_ATTR);
invariant(
!resultList.hasOwnProperty(resultIndex),
'Danger: Assigning to an already-occupied result index.'
);
resultList[resultIndex] = renderNode;
// This should match resultList.length and markupList.length when
// we're done.
resultListAssignmentCount += 1;
} else if (__DEV__) {
console.error(
'Danger: Discarding unexpected node:',
renderNode
);
}
}
}
// Although resultList was populated out of order, it should now be a dense
// array.
invariant(
resultListAssignmentCount === resultList.length,
'Danger: Did not assign to every index of resultList.'
);
invariant(
resultList.length === markupList.length,
'Danger: Expected markup to render %s nodes, but rendered %s.',
markupList.length,
resultList.length
);
return resultList;
},
/**
* Replaces a node with a string of markup at its current position within its
* parent. The markup must render into a single root node.
@@ -154,8 +154,8 @@ function assertValidProps(component, props) {
if (voidElementTags[component._tag]) {
invariant(
props.children == null && props.dangerouslySetInnerHTML == null,
'%s is a void element tag and must not have `children` or ' +
'use `props.dangerouslySetInnerHTML`.%s',
'%s is a void element tag and must neither have `children` nor ' +
'use `dangerouslySetInnerHTML`.%s',
component._tag,
component._currentElement._owner ?
' Check the render method of ' +
+16 -16
View File
@@ -11,6 +11,7 @@
'use strict';
var ReactDOMNullInputValuePropDevtool = require('ReactDOMNullInputValuePropDevtool');
var ReactDOMUnknownPropertyDevtool = require('ReactDOMUnknownPropertyDevtool');
var ReactDebugTool = require('ReactDebugTool');
@@ -20,23 +21,21 @@ var eventHandlers = [];
var handlerDoesThrowForEvent = {};
function emitEvent(handlerFunctionName, arg1, arg2, arg3, arg4, arg5) {
if (__DEV__) {
eventHandlers.forEach(function(handler) {
try {
if (handler[handlerFunctionName]) {
handler[handlerFunctionName](arg1, arg2, arg3, arg4, arg5);
}
} catch (e) {
warning(
handlerDoesThrowForEvent[handlerFunctionName],
'exception thrown by devtool while handling %s: %s',
handlerFunctionName,
e + '\n' + e.stack
);
handlerDoesThrowForEvent[handlerFunctionName] = true;
eventHandlers.forEach(function(handler) {
try {
if (handler[handlerFunctionName]) {
handler[handlerFunctionName](arg1, arg2, arg3, arg4, arg5);
}
});
}
} catch (e) {
warning(
handlerDoesThrowForEvent[handlerFunctionName],
'exception thrown by devtool while handling %s: %s',
handlerFunctionName,
e + '\n' + e.stack
);
handlerDoesThrowForEvent[handlerFunctionName] = true;
}
});
}
var ReactDOMDebugTool = {
@@ -68,5 +67,6 @@ var ReactDOMDebugTool = {
};
ReactDOMDebugTool.addDevtool(ReactDOMUnknownPropertyDevtool);
ReactDOMDebugTool.addDevtool(ReactDOMNullInputValuePropDevtool);
module.exports = ReactDOMDebugTool;
@@ -11,6 +11,11 @@
'use strict';
var ReactDOMDebugTool = require('ReactDOMDebugTool');
var debugTool = null;
module.exports = {debugTool: ReactDOMDebugTool};
if (__DEV__) {
var ReactDOMDebugTool = require('ReactDOMDebugTool');
debugTool = ReactDOMDebugTool;
}
module.exports = {debugTool};
@@ -1,101 +0,0 @@
/**
* Copyright 2013-present, 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';
describe('Danger', function() {
describe('dangerouslyRenderMarkup', function() {
var Danger;
beforeEach(function() {
jest.resetModuleRegistry();
Danger = require('Danger');
});
it('should render markup', function() {
var markup = '<div data-reactid=".rX"></div>';
var output = Danger.dangerouslyRenderMarkup([markup])[0];
expect(output.nodeName).toBe('DIV');
});
it('should render markup with props', function() {
var markup = '<div class="foo" data-reactid=".rX"></div>';
var output = Danger.dangerouslyRenderMarkup([markup])[0];
expect(output.nodeName).toBe('DIV');
expect(output.className).toBe('foo');
});
it('should render wrapped markup', function() {
var markup = '<th data-reactid=".rX"></th>';
var output = Danger.dangerouslyRenderMarkup([markup])[0];
expect(output.nodeName).toBe('TH');
});
it('should render lists of markup with similar `nodeName`', function() {
var renderedMarkup = Danger.dangerouslyRenderMarkup(
['<p id="A">1</p>', '<p id="B">2</p>', '<p id="C">3</p>']
);
expect(renderedMarkup.length).toBe(3);
expect(renderedMarkup[0].nodeName).toBe('P');
expect(renderedMarkup[1].nodeName).toBe('P');
expect(renderedMarkup[2].nodeName).toBe('P');
expect(renderedMarkup[0].innerHTML).toBe('1');
expect(renderedMarkup[1].innerHTML).toBe('2');
expect(renderedMarkup[2].innerHTML).toBe('3');
});
it('should render lists of markup with different `nodeName`', function() {
var renderedMarkup = Danger.dangerouslyRenderMarkup(
['<p id="A">1</p>', '<td id="B">2</td>', '<p id="C">3</p>']
);
expect(renderedMarkup.length).toBe(3);
expect(renderedMarkup[0].nodeName).toBe('P');
expect(renderedMarkup[1].nodeName).toBe('TD');
expect(renderedMarkup[2].nodeName).toBe('P');
expect(renderedMarkup[0].innerHTML).toBe('1');
expect(renderedMarkup[1].innerHTML).toBe('2');
expect(renderedMarkup[2].innerHTML).toBe('3');
});
it('should throw when rendering invalid markup', function() {
expect(function() {
Danger.dangerouslyRenderMarkup(['']);
}).toThrowError(
'dangerouslyRenderMarkup(...): Missing markup.'
);
spyOn(console, 'error');
var renderedMarkup = Danger.dangerouslyRenderMarkup(['<p></p><p></p>']);
var args = console.error.calls.argsFor(0);
expect(renderedMarkup.length).toBe(1);
expect(renderedMarkup[0].nodeName).toBe('P');
expect(console.error.calls.count()).toBe(1);
expect(args.length).toBe(2);
expect(args[0]).toBe('Danger: Discarding unexpected node:');
expect(args[1].nodeName).toBe('P');
});
});
});
@@ -811,8 +811,8 @@ describe('ReactDOMComponent', function() {
expect(function() {
ReactDOM.render(<input>children</input>, container);
}).toThrowError(
'input is a void element tag and must not have `children` or ' +
'use `props.dangerouslySetInnerHTML`.'
'input is a void element tag and must neither have `children` nor ' +
'use `dangerouslySetInnerHTML`.'
);
});
@@ -825,8 +825,8 @@ describe('ReactDOMComponent', function() {
container
);
}).toThrowError(
'input is a void element tag and must not have `children` or use ' +
'`props.dangerouslySetInnerHTML`.'
'input is a void element tag and must neither have `children` nor use ' +
'`dangerouslySetInnerHTML`.'
);
});
@@ -840,8 +840,8 @@ describe('ReactDOMComponent', function() {
expect(function() {
ReactDOM.render(<menu><menuitem>children</menuitem></menu>, container);
}).toThrowError(
'menuitem is a void element tag and must not have `children` or use ' +
'`props.dangerouslySetInnerHTML`.'
'menuitem is a void element tag and must neither have `children` nor use ' +
'`dangerouslySetInnerHTML`.'
);
});
@@ -977,8 +977,8 @@ describe('ReactDOMComponent', function() {
expect(function() {
ReactDOM.render(<X />, container);
}).toThrowError(
'input is a void element tag and must not have `children` ' +
'or use `props.dangerouslySetInnerHTML`. Check the render method of X.'
'input is a void element tag and must neither have `children` ' +
'nor use `dangerouslySetInnerHTML`. Check the render method of X.'
);
});
@@ -1007,8 +1007,8 @@ describe('ReactDOMComponent', function() {
expect(function() {
ReactDOM.render(<input>children</input>, container);
}).toThrowError(
'input is a void element tag and must not have `children` or use ' +
'`props.dangerouslySetInnerHTML`.'
'input is a void element tag and must neither have `children` nor use ' +
'`dangerouslySetInnerHTML`.'
);
});
@@ -1021,8 +1021,8 @@ describe('ReactDOMComponent', function() {
container
);
}).toThrowError(
'input is a void element tag and must not have `children` or use ' +
'`props.dangerouslySetInnerHTML`.'
'input is a void element tag and must neither have `children` nor use ' +
'`dangerouslySetInnerHTML`.'
);
});
@@ -0,0 +1,50 @@
/**
* Copyright 2013-present, 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 ReactDOMNullInputValuePropDevtool
*/
'use strict';
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
var warning = require('warning');
var didWarnValueNull = false;
function handleElement(debugID, element) {
if (element == null) {
return;
}
if (element.type !== 'input' && element.type !== 'textarea' && element.type !== 'select') {
return;
}
if (element.props != null && element.props.value === null && !didWarnValueNull) {
warning(
false,
'`value` prop on `%s` should not be null. ' +
'Consider using the empty string to clear the component or `undefined` ' +
'for uncontrolled components.%s',
element.type,
ReactComponentTreeDevtool.getStackAddendumByID(debugID)
);
didWarnValueNull = true;
}
}
var ReactDOMUnknownPropertyDevtool = {
onBeforeMountComponent(debugID, element) {
handleElement(debugID, element);
},
onBeforeUpdateComponent(debugID, element) {
handleElement(debugID, element);
},
};
module.exports = ReactDOMUnknownPropertyDevtool;
+2 -13
View File
@@ -118,7 +118,7 @@ var ReactNativeMount = {
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props;
if (shouldUpdateReactComponent(prevElement, nextElement)) {
ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement);
ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement, emptyObject);
if (callback) {
ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
}
@@ -135,19 +135,9 @@ var ReactNativeMount = {
ReactNativeTagHandles.assertRootTag(containerTag);
var instance = instantiateReactComponent(nextWrappedElement);
var instance = instantiateReactComponent(nextWrappedElement, false);
ReactNativeMount._instancesByContainerID[containerTag] = instance;
if (__DEV__) {
// Mute future events from the top level wrapper.
// It is an implementation detail that devtools should not know about.
instance._debugID = 0;
if (__DEV__) {
ReactInstrumentation.debugTool.onBeginFlush();
}
}
// The initial render is synchronous but any updates that happen during
// rendering, in componentWillMount or componentDidMount, will be batched
// according to the current batching strategy.
@@ -162,7 +152,6 @@ var ReactNativeMount = {
ReactInstrumentation.debugTool.onMountRootComponent(
instance._renderedComponent._debugID
);
ReactInstrumentation.debugTool.onEndFlush();
}
var component = instance.getPublicInstance();
if (callback) {
@@ -14,6 +14,8 @@
var CallbackQueue = require('CallbackQueue');
var PooledClass = require('PooledClass');
var Transaction = require('Transaction');
var ReactInstrumentation = require('ReactInstrumentation');
var ReactUpdateQueue = require('ReactUpdateQueue');
/**
* Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during
@@ -42,6 +44,13 @@ var ON_DOM_READY_QUEUEING = {
*/
var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
if (__DEV__) {
TRANSACTION_WRAPPERS.push({
initialize: ReactInstrumentation.debugTool.onBeginFlush,
close: ReactInstrumentation.debugTool.onEndFlush,
});
}
/**
* Currently:
* - The order that these are listed in the transaction is critical:
@@ -81,6 +90,13 @@ var Mixin = {
return this.reactMountReady;
},
/**
* @return {object} The queue to collect React async events.
*/
getUpdateQueue: function() {
return ReactUpdateQueue;
},
/**
* `PooledClass` looks for this, and will invoke this before allowing this
* instance to be reused.
+110 -5
View File
@@ -19,31 +19,101 @@
'use strict';
import type { Fiber } from 'ReactFiber';
import type { HostChildren } from 'ReactFiberReconciler';
var ReactFiberReconciler = require('ReactFiberReconciler');
var scheduledHighPriCallback = null;
var scheduledLowPriCallback = null;
const TERMINAL_TAG = 99;
type Container = { rootID: number, children: Array<Instance> };
type Props = { };
type Instance = { tag: 99, type: string, id: number, children: Array<Instance> };
var instanceCounter = 0;
function recursivelyAppendChildren(flatArray : Array<Instance>, child : HostChildren<Instance>) {
if (!child) {
return;
}
if (child.tag === TERMINAL_TAG) {
flatArray.push(child);
} else {
let node = child;
do {
recursivelyAppendChildren(flatArray, node.output);
} while (node = node.sibling);
}
}
function flattenChildren(children : HostChildren<Instance>) {
const flatArray = [];
recursivelyAppendChildren(flatArray, children);
return flatArray;
}
var NoopRenderer = ReactFiberReconciler({
createHostInstance() {
updateContainer(containerInfo : Container, children : HostChildren<Instance>) : void {
console.log('Update container #' + containerInfo.rootID);
containerInfo.children = flattenChildren(children);
},
createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
console.log('Create instance #' + instanceCounter);
const inst = {
tag: TERMINAL_TAG,
id: instanceCounter++,
type: type,
children: flattenChildren(children),
};
// Hide from unit tests
Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false });
Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false });
return inst;
},
prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : boolean {
console.log('Prepare for update on #' + instance.id);
return true;
},
commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
console.log('Commit update on #' + instance.id);
instance.children = flattenChildren(children);
},
deleteInstance(instance : Instance) : void {
console.log('Delete #' + instance.id);
},
scheduleHighPriCallback(callback) {
scheduledHighPriCallback = callback;
},
scheduleLowPriCallback(callback) {
scheduledLowPriCallback = callback;
},
});
var rootContainer = { rootID: 0, children: [] };
var root = null;
var ReactNoop = {
root: rootContainer,
render(element : ReactElement<any>) {
NoopRenderer.mountNewRoot(element);
if (!root) {
root = NoopRenderer.mountContainer(element, rootContainer);
} else {
NoopRenderer.updateContainer(element, root);
}
},
flushHighPri() {
@@ -79,6 +149,41 @@ var ReactNoop = {
ReactNoop.flushLowPri();
},
// Logs the current state of the tree.
dumpTree() {
if (!root) {
console.log('Nothing rendered yet.');
return;
}
function logHostInstances(children: Array<Instance>, depth) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
console.log(' '.repeat(depth) + '- ' + child.type + '#' + child.id);
logHostInstances(child.children, depth + 1);
}
}
function logContainer(container : Container, depth) {
console.log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
logHostInstances(container.children, depth + 1);
}
function logFiber(fiber : Fiber, depth) {
console.log(' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']');
if (fiber.child) {
logFiber(fiber.child, depth + 1);
}
if (fiber.sibling) {
logFiber(fiber.sibling, depth);
}
}
console.log('HOST INSTANCES:');
logContainer(rootContainer, 0);
console.log('FIBERS:');
logFiber((root.stateNode : any).current, 0);
},
};
module.exports = ReactNoop;
+80 -88
View File
@@ -11,6 +11,9 @@
'use strict';
var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool');
var ReactHostOperationHistoryDevtool = require('ReactHostOperationHistoryDevtool');
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
var ExecutionEnvironment = require('ExecutionEnvironment');
var performanceNow = require('performanceNow');
@@ -20,23 +23,21 @@ var eventHandlers = [];
var handlerDoesThrowForEvent = {};
function emitEvent(handlerFunctionName, arg1, arg2, arg3, arg4, arg5) {
if (__DEV__) {
eventHandlers.forEach(function(handler) {
try {
if (handler[handlerFunctionName]) {
handler[handlerFunctionName](arg1, arg2, arg3, arg4, arg5);
}
} catch (e) {
warning(
handlerDoesThrowForEvent[handlerFunctionName],
'exception thrown by devtool while handling %s: %s',
handlerFunctionName,
e + '\n' + e.stack
);
handlerDoesThrowForEvent[handlerFunctionName] = true;
eventHandlers.forEach(function(handler) {
try {
if (handler[handlerFunctionName]) {
handler[handlerFunctionName](arg1, arg2, arg3, arg4, arg5);
}
});
}
} catch (e) {
warning(
handlerDoesThrowForEvent[handlerFunctionName],
'exception thrown by devtool while handling %s: %s',
handlerFunctionName,
e + '\n' + e.stack
);
handlerDoesThrowForEvent[handlerFunctionName] = true;
}
});
}
var isProfiling = false;
@@ -73,32 +74,30 @@ function getTreeSnapshot(registeredIDs) {
}
function resetMeasurements() {
if (__DEV__) {
var previousStartTime = currentFlushStartTime;
var previousMeasurements = currentFlushMeasurements || [];
var previousOperations = ReactHostOperationHistoryDevtool.getHistory();
if (!isProfiling || currentFlushNesting === 0) {
currentFlushStartTime = null;
currentFlushMeasurements = null;
clearHistory();
return;
}
if (previousMeasurements.length || previousOperations.length) {
var registeredIDs = ReactComponentTreeDevtool.getRegisteredIDs();
flushHistory.push({
duration: performanceNow() - previousStartTime,
measurements: previousMeasurements || [],
operations: previousOperations || [],
treeSnapshot: getTreeSnapshot(registeredIDs),
});
}
var previousStartTime = currentFlushStartTime;
var previousMeasurements = currentFlushMeasurements || [];
var previousOperations = ReactHostOperationHistoryDevtool.getHistory();
if (currentFlushNesting === 0) {
currentFlushStartTime = null;
currentFlushMeasurements = null;
clearHistory();
currentFlushStartTime = performanceNow();
currentFlushMeasurements = [];
return;
}
if (previousMeasurements.length || previousOperations.length) {
var registeredIDs = ReactComponentTreeDevtool.getRegisteredIDs();
flushHistory.push({
duration: performanceNow() - previousStartTime,
measurements: previousMeasurements || [],
operations: previousOperations || [],
treeSnapshot: getTreeSnapshot(registeredIDs),
});
}
clearHistory();
currentFlushStartTime = performanceNow();
currentFlushMeasurements = [];
}
function checkDebugID(debugID) {
@@ -106,7 +105,7 @@ function checkDebugID(debugID) {
}
function beginLifeCycleTimer(debugID, timerType) {
if (!isProfiling || currentFlushNesting === 0) {
if (currentFlushNesting === 0) {
return;
}
warning(
@@ -125,7 +124,7 @@ function beginLifeCycleTimer(debugID, timerType) {
}
function endLifeCycleTimer(debugID, timerType) {
if (!isProfiling || currentFlushNesting === 0) {
if (currentFlushNesting === 0) {
return;
}
warning(
@@ -137,11 +136,13 @@ function endLifeCycleTimer(debugID, timerType) {
currentTimerType || 'no',
(debugID === currentTimerDebugID) ? 'the same' : 'another'
);
currentFlushMeasurements.push({
timerType,
instanceID: debugID,
duration: performanceNow() - currentTimerStartTime - currentTimerNestedFlushDuration,
});
if (isProfiling) {
currentFlushMeasurements.push({
timerType,
instanceID: debugID,
duration: performanceNow() - currentTimerStartTime - currentTimerNestedFlushDuration,
});
}
currentTimerStartTime = null;
currentTimerNestedFlushDuration = null;
currentTimerDebugID = null;
@@ -187,57 +188,47 @@ var ReactDebugTool = {
return isProfiling;
},
beginProfiling() {
if (__DEV__) {
if (isProfiling) {
return;
}
isProfiling = true;
flushHistory.length = 0;
resetMeasurements();
if (isProfiling) {
return;
}
isProfiling = true;
flushHistory.length = 0;
resetMeasurements();
ReactDebugTool.addDevtool(ReactHostOperationHistoryDevtool);
},
endProfiling() {
if (__DEV__) {
if (!isProfiling) {
return;
}
isProfiling = false;
resetMeasurements();
if (!isProfiling) {
return;
}
isProfiling = false;
resetMeasurements();
ReactDebugTool.removeDevtool(ReactHostOperationHistoryDevtool);
},
getFlushHistory() {
return flushHistory;
},
onBeginFlush() {
if (__DEV__) {
currentFlushNesting++;
resetMeasurements();
pauseCurrentLifeCycleTimer();
}
currentFlushNesting++;
resetMeasurements();
pauseCurrentLifeCycleTimer();
emitEvent('onBeginFlush');
},
onEndFlush() {
if (__DEV__) {
resetMeasurements();
currentFlushNesting--;
resumeCurrentLifeCycleTimer();
}
resetMeasurements();
currentFlushNesting--;
resumeCurrentLifeCycleTimer();
emitEvent('onEndFlush');
},
onBeginLifeCycleTimer(debugID, timerType) {
checkDebugID(debugID);
emitEvent('onBeginLifeCycleTimer', debugID, timerType);
if (__DEV__) {
beginLifeCycleTimer(debugID, timerType);
}
beginLifeCycleTimer(debugID, timerType);
},
onEndLifeCycleTimer(debugID, timerType) {
checkDebugID(debugID);
if (__DEV__) {
endLifeCycleTimer(debugID, timerType);
}
endLifeCycleTimer(debugID, timerType);
emitEvent('onEndLifeCycleTimer', debugID, timerType);
},
onBeginReconcilerTimer(debugID, timerType) {
@@ -248,6 +239,12 @@ var ReactDebugTool = {
checkDebugID(debugID);
emitEvent('onEndReconcilerTimer', debugID, timerType);
},
onError(debugID) {
if (currentTimerDebugID != null) {
endLifeCycleTimer(currentTimerDebugID, currentTimerType);
}
emitEvent('onError', debugID);
},
onBeginProcessingChildContext() {
emitEvent('onBeginProcessingChildContext');
},
@@ -267,6 +264,7 @@ var ReactDebugTool = {
},
onSetChildren(debugID, childDebugIDs) {
checkDebugID(debugID);
childDebugIDs.forEach(checkDebugID);
emitEvent('onSetChildren', debugID, childDebugIDs);
},
onSetOwner(debugID, ownerDebugID) {
@@ -310,17 +308,11 @@ var ReactDebugTool = {
},
};
if (__DEV__) {
var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool');
var ReactHostOperationHistoryDevtool = require('ReactHostOperationHistoryDevtool');
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool);
ReactDebugTool.addDevtool(ReactComponentTreeDevtool);
ReactDebugTool.addDevtool(ReactHostOperationHistoryDevtool);
var url = (ExecutionEnvironment.canUseDOM && window.location.href) || '';
if ((/[?&]react_perf\b/).test(url)) {
ReactDebugTool.beginProfiling();
}
ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool);
ReactDebugTool.addDevtool(ReactComponentTreeDevtool);
var url = (ExecutionEnvironment.canUseDOM && window.location.href) || '';
if ((/[?&]react_perf\b/).test(url)) {
ReactDebugTool.beginProfiling();
}
module.exports = ReactDebugTool;
+7 -2
View File
@@ -11,6 +11,11 @@
'use strict';
var ReactDebugTool = require('ReactDebugTool');
var debugTool = null;
module.exports = {debugTool: ReactDebugTool};
if (__DEV__) {
var ReactDebugTool = require('ReactDebugTool');
debugTool = ReactDebugTool;
}
module.exports = {debugTool};
@@ -467,5 +467,49 @@ describe('ReactPerf', function() {
expect(console.error.calls.count()).toBe(1);
__DEV__ = true;
})
});
it('should work when measurement starts during reconciliation', () => {
// https://github.com/facebook/react/issues/6949#issuecomment-230371009
var Measurer = React.createClass({
componentWillMount() {
ReactPerf.start();
},
componentDidMount() {
ReactPerf.stop();
},
componentWillUpdate() {
ReactPerf.start();
},
componentDidUpdate() {
ReactPerf.stop();
},
render() {
// Force reconciliation despite constant element
return React.cloneElement(this.props.children);
},
});
var container = document.createElement('div');
ReactDOM.render(<Measurer><App /></Measurer>, container);
expect(ReactPerf.getWasted()).toEqual([]);
ReactDOM.render(<Measurer><App /></Measurer>, container);
expect(ReactPerf.getWasted()).toEqual([{
key: 'Measurer',
instanceCount: 1,
inclusiveRenderDuration: 4,
renderCount: 1,
}, {
key: 'App',
instanceCount: 1,
inclusiveRenderDuration: 3,
renderCount: 1,
}, {
key: 'App > Box',
instanceCount: 2,
inclusiveRenderDuration: 2,
renderCount: 2,
}]);
});
});
@@ -1716,103 +1716,137 @@ describe('ReactComponentTreeDevtool', () => {
expect(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
});
it('creates current stack addenda', () => {
function getAddendum(element) {
var addendum = ReactComponentTreeDevtool.getCurrentStackAddendum(element);
return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
}
var Anon = React.createClass({displayName: null, render: () => null});
var Orange = React.createClass({render: () => null});
expect(getAddendum()).toBe(
''
);
expect(getAddendum(<div />)).toBe(
'\n in div (at **)'
);
expect(getAddendum(<Anon />)).toBe(
'\n in Unknown (at **)'
);
expect(getAddendum(<Orange />)).toBe(
'\n in Orange (at **)'
);
expect(getAddendum(React.createElement(Orange))).toBe(
'\n in Orange'
);
var renders = 0;
var rOwnedByQ;
function Q() {
return (rOwnedByQ = React.createElement(R));
}
function R() {
return <div><S /></div>;
}
class S extends React.Component {
componentDidMount() {
// Check that the parent path is still fetched when only S itself is on
// the stack.
this.forceUpdate();
describe('stack addenda', () => {
it('gets created', () => {
function getAddendum(element) {
var addendum = ReactComponentTreeDevtool.getCurrentStackAddendum(element);
return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
}
render() {
expect(getAddendum()).toBe(
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
expect(getAddendum(<span />)).toBe(
'\n in span (at **)' +
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
expect(getAddendum(React.createElement('span'))).toBe(
'\n in span (created by S)' +
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
renders++;
return null;
var Anon = React.createClass({displayName: null, render: () => null});
var Orange = React.createClass({render: () => null});
expect(getAddendum()).toBe(
''
);
expect(getAddendum(<div />)).toBe(
'\n in div (at **)'
);
expect(getAddendum(<Anon />)).toBe(
'\n in Unknown (at **)'
);
expect(getAddendum(<Orange />)).toBe(
'\n in Orange (at **)'
);
expect(getAddendum(React.createElement(Orange))).toBe(
'\n in Orange'
);
var renders = 0;
var rOwnedByQ;
function Q() {
return (rOwnedByQ = React.createElement(R));
}
}
ReactDOM.render(<Q />, document.createElement('div'));
expect(renders).toBe(2);
// Make sure owner is fetched for the top element too.
expect(getAddendum(rOwnedByQ)).toBe(
'\n in R (created by Q)'
);
});
it('creates stack addenda by ID', () => {
function getAddendum(id) {
var addendum = ReactComponentTreeDevtool.getStackAddendumByID(id);
return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
}
class Q extends React.Component {
render() {
return null;
function R() {
return <div><S /></div>;
}
}
class S extends React.Component {
componentDidMount() {
// Check that the parent path is still fetched when only S itself is on
// the stack.
this.forceUpdate();
}
render() {
expect(getAddendum()).toBe(
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
expect(getAddendum(<span />)).toBe(
'\n in span (at **)' +
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
expect(getAddendum(React.createElement('span'))).toBe(
'\n in span (created by S)' +
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
renders++;
return null;
}
}
ReactDOM.render(<Q />, document.createElement('div'));
expect(renders).toBe(2);
var q = ReactDOM.render(<Q />, document.createElement('div'));
expect(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe(
'\n in Q (at **)'
);
// Make sure owner is fetched for the top element too.
expect(getAddendum(rOwnedByQ)).toBe(
'\n in R (created by Q)'
);
});
spyOn(console, 'error');
getAddendum(-17);
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toBe(
'Warning: ReactComponentTreeDevtool: Missing React element for debugID ' +
'-17 when building stack'
);
});
it('can be retrieved by ID', () => {
function getAddendum(id) {
var addendum = ReactComponentTreeDevtool.getStackAddendumByID(id);
return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
}
class Q extends React.Component {
render() {
return null;
}
}
var q = ReactDOM.render(<Q />, document.createElement('div'));
expect(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe(
'\n in Q (at **)'
);
spyOn(console, 'error');
getAddendum(-17);
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toBe(
'Warning: ReactComponentTreeDevtool: Missing React element for ' +
'debugID -17 when building stack'
);
});
it('is created during mounting', () => {
// https://github.com/facebook/react/issues/7187
var el = document.createElement('div');
var portalEl = document.createElement('div');
class Foo extends React.Component {
componentWillMount() {
ReactDOM.render(<div />, portalEl);
}
render() {
return <div><div /></div>;
}
}
ReactDOM.render(<Foo />, el);
});
it('is created when calling renderToString during render', () => {
// https://github.com/facebook/react/issues/7190
var el = document.createElement('div');
class Foo extends React.Component {
render() {
return (
<div>
<div>
{ReactDOMServer.renderToString(<div />)}
</div>
</div>
);
}
}
ReactDOM.render(<Foo />, el);
});
})
});
@@ -13,6 +13,7 @@
describe('ReactHostOperationHistoryDevtool', () => {
var React;
var ReactPerf;
var ReactDOM;
var ReactDOMComponentTree;
var ReactDOMFeatureFlags;
@@ -22,12 +23,19 @@ describe('ReactHostOperationHistoryDevtool', () => {
jest.resetModuleRegistry();
React = require('React');
ReactPerf = require('ReactPerf');
ReactDOM = require('ReactDOM');
ReactDOMComponentTree = require('ReactDOMComponentTree');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactHostOperationHistoryDevtool = require('ReactHostOperationHistoryDevtool');
ReactPerf.start();
});
afterEach(() => {
ReactPerf.stop();
})
function assertHistoryMatches(expectedHistory) {
var actualHistory = ReactHostOperationHistoryDevtool.getHistory();
expect(actualHistory).toEqual(expectedHistory);
+28 -23
View File
@@ -14,6 +14,7 @@
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { PriorityLevel } from 'ReactPriorityLevel';
import type { ReactNodeList } from 'ReactTypes';
@@ -28,7 +29,7 @@ var {
var ReactFiber = require('ReactFiber');
var ReactReifiedYield = require('ReactReifiedYield');
function createSubsequentChild(parent : Fiber, existingChild : ?Fiber, previousSibling : Fiber, newChildren) : Fiber {
function createSubsequentChild(returnFiber : Fiber, existingChild : ?Fiber, previousSibling : Fiber, newChildren, priority : PriorityLevel) : Fiber {
if (typeof newChildren !== 'object' || newChildren === null) {
return previousSibling;
}
@@ -41,34 +42,35 @@ function createSubsequentChild(parent : Fiber, existingChild : ?Fiber, previousS
element.key === existingChild.key) {
// TODO: This is not sufficient since previous siblings could be new.
// Will fix reconciliation properly later.
const clone = ReactFiber.cloneFiber(existingChild);
clone.input = element.props;
const clone = ReactFiber.cloneFiber(existingChild, priority);
clone.pendingProps = element.props;
clone.child = existingChild.child;
clone.sibling = null;
clone.return = returnFiber;
previousSibling.sibling = clone;
return clone;
}
const child = ReactFiber.createFiberFromElement(element);
const child = ReactFiber.createFiberFromElement(element, priority);
previousSibling.sibling = child;
child.parent = parent;
child.return = returnFiber;
return child;
}
case REACT_COROUTINE_TYPE: {
const coroutine = (newChildren : ReactCoroutine);
const child = ReactFiber.createFiberFromCoroutine(coroutine);
const child = ReactFiber.createFiberFromCoroutine(coroutine, priority);
previousSibling.sibling = child;
child.parent = parent;
child.return = returnFiber;
return child;
}
case REACT_YIELD_TYPE: {
const yieldNode = (newChildren : ReactYield);
const reifiedYield = ReactReifiedYield.createReifiedYield(yieldNode);
const child = ReactFiber.createFiberFromYield(yieldNode);
const child = ReactFiber.createFiberFromYield(yieldNode, priority);
child.output = reifiedYield;
previousSibling.sibling = child;
child.parent = parent;
child.return = returnFiber;
return child;
}
}
@@ -77,7 +79,7 @@ function createSubsequentChild(parent : Fiber, existingChild : ?Fiber, previousS
let prev : Fiber = previousSibling;
let existing : ?Fiber = existingChild;
for (var i = 0; i < newChildren.length; i++) {
prev = createSubsequentChild(parent, existing, prev, newChildren[i]);
prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
if (prev && existing) {
// TODO: This is not correct because there could've been more
// than one sibling consumed but I don't want to return a tuple.
@@ -91,7 +93,7 @@ function createSubsequentChild(parent : Fiber, existingChild : ?Fiber, previousS
}
}
function createFirstChild(parent, existingChild, newChildren) {
function createFirstChild(returnFiber, existingChild, newChildren, priority) {
if (typeof newChildren !== 'object' || newChildren === null) {
return null;
}
@@ -103,21 +105,22 @@ function createFirstChild(parent, existingChild, newChildren) {
element.type === existingChild.type &&
element.key === existingChild.key) {
// Get the clone of the existing fiber.
const clone = ReactFiber.cloneFiber(existingChild);
clone.input = element.props;
const clone = ReactFiber.cloneFiber(existingChild, priority);
clone.pendingProps = element.props;
clone.child = existingChild.child;
clone.sibling = null;
clone.return = returnFiber;
return clone;
}
const child = ReactFiber.createFiberFromElement(element);
child.parent = parent;
const child = ReactFiber.createFiberFromElement(element, priority);
child.return = returnFiber;
return child;
}
case REACT_COROUTINE_TYPE: {
const coroutine = (newChildren : ReactCoroutine);
const child = ReactFiber.createFiberFromCoroutine(coroutine);
child.parent = parent;
const child = ReactFiber.createFiberFromCoroutine(coroutine, priority);
child.return = returnFiber;
return child;
}
@@ -127,9 +130,9 @@ function createFirstChild(parent, existingChild, newChildren) {
// the fragment.
const yieldNode = (newChildren : ReactYield);
const reifiedYield = ReactReifiedYield.createReifiedYield(yieldNode);
const child = ReactFiber.createFiberFromYield(yieldNode);
const child = ReactFiber.createFiberFromYield(yieldNode, priority);
child.output = reifiedYield;
child.parent = parent;
child.return = returnFiber;
return child;
}
}
@@ -140,10 +143,10 @@ function createFirstChild(parent, existingChild, newChildren) {
var existing : ?Fiber = existingChild;
for (var i = 0; i < newChildren.length; i++) {
if (prev == null) {
prev = createFirstChild(parent, existing, newChildren[i]);
prev = createFirstChild(returnFiber, existing, newChildren[i], priority);
first = prev;
} else {
prev = createSubsequentChild(parent, existing, prev, newChildren[i]);
prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
}
if (prev && existing) {
// TODO: This is not correct because there could've been more
@@ -158,6 +161,8 @@ function createFirstChild(parent, existingChild, newChildren) {
}
}
exports.reconcileChildFibers = function(parent : Fiber, currentFirstChild : ?Fiber, newChildren : ReactNodeList) : ?Fiber {
return createFirstChild(parent, currentFirstChild, newChildren);
// TODO: This API won't work because we'll need to transfer the side-effects of
// unmounting children to the returnFiber.
exports.reconcileChildFibers = function(returnFiber : Fiber, currentFirstChild : ?Fiber, newChildren : ReactNodeList, priority : PriorityLevel) : ?Fiber {
return createFirstChild(returnFiber, currentFirstChild, newChildren, priority);
};
+96 -37
View File
@@ -12,18 +12,25 @@
'use strict';
var ReactTypesOfWork = require('ReactTypesOfWork');
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
import type { TypeOfWork } from 'ReactTypeOfWork';
import type { PriorityLevel } from 'ReactPriorityLevel';
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
IndeterminateComponent,
ClassComponent,
HostContainer,
HostComponent,
CoroutineComponent,
YieldComponent,
} = ReactTypesOfWork;
} = ReactTypeOfWork;
var ReactElement = require('ReactElement');
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
var {
NoWork,
} = require('ReactPriorityLevel');
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
@@ -32,12 +39,7 @@ import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
type Instance = {
// Tag identifying the type of fiber.
tag: number,
// The parent Fiber used to create this one. The type is constrained to the
// Instance part of the Fiber since it is not safe to traverse the tree from
// the instance.
parent: ?Instance, // Consider a regenerated temporary parent stack instead.
tag: TypeOfWork,
// Unique identifier of this child.
key: null | string,
@@ -46,7 +48,11 @@ type Instance = {
type: any,
// The local state associated with this fiber.
stateNode: ?Object,
stateNode: any,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
};
@@ -54,6 +60,12 @@ type Instance = {
// be more than one per component.
export type Fiber = Instance & {
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: ?Fiber,
// Singly Linked List Tree Structure.
child: ?Fiber,
sibling: ?Fiber,
@@ -63,32 +75,51 @@ export type Fiber = Instance & {
ref: null | (handle : ?Object) => void,
// Input is the data coming into process this fiber. Arguments. Props.
input: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge input and memoizedInput somehow.
memoizedInput: any, // The input used to create the output.
pendingProps: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge pendingProps and memoizedProps.
memoizedProps: any, // The props used to create the output.
// Output is the return value of this fiber, or a linked list of return values
// if this returns multiple values. Such as a fragment.
output: any, // This type will be more specific once we overload the tag.
// Singly linked list fast path to the next fiber with side-effects.
nextEffect: ?Fiber,
// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: ?Fiber,
lastEffect: ?Fiber,
// This will be used to quickly determine if a subtree has no pending changes.
hasPendingChanges: bool,
pendingWorkPriority: PriorityLevel,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: ?Fiber,
// Keeps track if we've completed this node or if we're currently in the
// middle of processing it. We really should know this based on pendingProps
// or something else. We could also reuse the tag for this purpose. However,
// I'm not really sure so I'll use a flag for now.
// TODO: Find another way to infer this flag.
hasWorkInProgress: bool,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
};
var createFiber = function(tag : number, key : null | string) : Fiber {
var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
return {
// Instance
tag: tag,
parent: null,
key: key,
type: null,
@@ -97,16 +128,24 @@ var createFiber = function(tag : number, key : null | string) : Fiber {
// Fiber
return: null,
child: null,
sibling: null,
ref: null,
input: null,
memoizedInput: null,
pendingProps: null,
memoizedProps: null,
output: null,
hasPendingChanges: true,
nextEffect: null,
firstEffect: null,
lastEffect: null,
pendingWorkPriority: NoWork,
hasWorkInProgress: false,
alternate: null,
@@ -118,35 +157,53 @@ function shouldConstruct(Component) {
}
// This is used to create an alternate fiber to do work on.
exports.cloneFiber = function(fiber : Fiber) : Fiber {
exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fiber {
// We use a double buffering pooling technique because we know that we'll only
// ever need at most two versions of a tree. We pool the "other" unused node
// that we're free to reuse. This is lazily created to avoid allocating extra
// objects for things that are never updated. It also allow us to reclaim the
// extra memory if needed.
if (fiber.alternate) {
return fiber.alternate;
let alt = fiber.alternate;
if (alt) {
alt.stateNode = fiber.stateNode;
alt.child = fiber.child;
alt.sibling = fiber.sibling;
alt.ref = alt.ref;
alt.pendingProps = fiber.pendingProps;
alt.pendingWorkPriority = priorityLevel;
// Whenever we clone, we do so to get a new work in progress.
// This ensures that we've reset these in the new tree.
alt.nextEffect = null;
alt.firstEffect = null;
alt.lastEffect = null;
return alt;
}
// This should not have an alternate already
var alt = createFiber(fiber.tag, fiber.key);
if (fiber.parent) {
// TODO: This assumes the parent's alternate is already created.
// Stop using the alternates of parents once we have a parent stack.
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
alt.parent = fiber.parent.alternate;
}
alt = createFiber(fiber.tag, fiber.key);
alt.type = fiber.type;
alt.stateNode = fiber.stateNode;
alt.child = fiber.child;
alt.sibling = fiber.sibling;
alt.ref = alt.ref;
alt.pendingWorkPriority = priorityLevel;
alt.alternate = fiber;
fiber.alternate = alt;
return alt;
};
exports.createFiberFromElement = function(element : ReactElement) {
exports.createHostContainerFiber = function() {
const fiber = createFiber(HostContainer, null);
return fiber;
};
exports.createFiberFromElement = function(element : ReactElement, priorityLevel : PriorityLevel) {
const fiber = exports.createFiberFromElementType(element.type, element.key);
fiber.input = element.props;
fiber.pendingProps = element.props;
fiber.pendingWorkPriority = priorityLevel;
return fiber;
};
@@ -169,14 +226,16 @@ exports.createFiberFromElementType = function(type : mixed, key : null | string)
return fiber;
};
exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine) {
exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine, priorityLevel : PriorityLevel) {
const fiber = createFiber(CoroutineComponent, coroutine.key);
fiber.type = coroutine.handler;
fiber.input = coroutine;
fiber.pendingProps = coroutine;
fiber.pendingWorkPriority = priorityLevel;
return fiber;
};
exports.createFiberFromYield = function(yieldNode : ReactYield) {
exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel : PriorityLevel) {
const fiber = createFiber(YieldComponent, yieldNode.key);
fiber.pendingProps = {};
return fiber;
};
+215 -105
View File
@@ -14,127 +14,237 @@
import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';
var ReactChildFiber = require('ReactChildFiber');
var ReactTypesOfWork = require('ReactTypesOfWork');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
IndeterminateComponent,
FunctionalComponent,
ClassComponent,
HostContainer,
HostComponent,
CoroutineComponent,
CoroutineHandlerPhase,
YieldComponent,
} = ReactTypesOfWork;
} = ReactTypeOfWork;
var {
NoWork,
OffscreenPriority,
} = require('ReactPriorityLevel');
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
function reconcileChildren(current, workInProgress, nextChildren) {
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
current ? current.child : null,
nextChildren
);
}
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
function updateFunctionalComponent(current, workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.input;
console.log('update fn:', fn.name);
var nextChildren = fn(props);
reconcileChildren(current, workInProgress, nextChildren);
}
function updateHostComponent(current, workInProgress) {
console.log('host component', workInProgress.type, typeof workInProgress.input.children === 'string' ? workInProgress.input.children : '');
var nextChildren = workInProgress.input.children;
reconcileChildren(current, workInProgress, nextChildren);
}
function mountIndeterminateComponent(current, workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.input;
var value = fn(props);
if (typeof value === 'object' && value && typeof value.render === 'function') {
console.log('performed work on class:', fn.name);
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
} else {
console.log('performed work on fn:', fn.name);
// Proceed under the assumption that this is a functional component
workInProgress.tag = FunctionalComponent;
function reconcileChildren(current, workInProgress, nextChildren) {
const priority = workInProgress.pendingWorkPriority;
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
current ? current.child : null,
nextChildren,
priority
);
}
reconcileChildren(current, workInProgress, value);
}
function updateCoroutineComponent(current, workInProgress) {
var coroutine = (workInProgress.input : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
function updateFunctionalComponent(current, workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.pendingProps;
console.log('update fn:', fn.name);
var nextChildren = fn(props);
reconcileChildren(current, workInProgress, nextChildren);
workInProgress.pendingWorkPriority = NoWork;
}
console.log('begin coroutine', workInProgress.type.name);
reconcileChildren(current, workInProgress, coroutine.children);
}
function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
if (current && workInProgress.input === current.memoizedInput) {
// The most likely scenario is that the previous copy of the tree contains
// the same input as the new one. In that case, we can just copy the output
// and children from that node.
workInProgress.output = current.output;
workInProgress.child = current.child;
workInProgress.stateNode = current.stateNode;
return null;
}
if (workInProgress.input === workInProgress.memoizedInput) {
// In a ping-pong scenario, this version could actually contain the
// old input. In that case, we can just bail out.
return null;
}
switch (workInProgress.tag) {
case IndeterminateComponent:
mountIndeterminateComponent(current, workInProgress);
return workInProgress.child;
case FunctionalComponent:
updateFunctionalComponent(current, workInProgress);
return workInProgress.child;
case ClassComponent:
console.log('class component', workInProgress.input.type.name);
return workInProgress.child;
case HostComponent:
updateHostComponent(current, workInProgress);
return workInProgress.child;
case CoroutineHandlerPhase:
// This is a restart. Reset the tag to the initial phase.
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
updateCoroutineComponent(current, workInProgress);
// This doesn't take arbitrary time so we could synchronously just begin
// eagerly do the work of workInProgress.child as an optimization.
if (workInProgress.child) {
return beginWork(
workInProgress.child.alternate,
workInProgress.child
);
}
return workInProgress.child;
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
if (workInProgress.sibling) {
return beginWork(
workInProgress.sibling.alternate,
workInProgress.sibling
);
}
function updateHostComponent(current, workInProgress) {
console.log('host component', workInProgress.type, typeof workInProgress.pendingProps.children === 'string' ? workInProgress.pendingProps.children : '');
var nextChildren = workInProgress.pendingProps.children;
let priority = workInProgress.pendingWorkPriority;
if (workInProgress.pendingProps.hidden && priority !== OffscreenPriority) {
// If this host component is hidden, we can reconcile its children at
// the lowest priority and bail out from this particular pass. Unless, we're
// currently reconciling the lowest priority.
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
current ? current.child : null,
nextChildren,
OffscreenPriority
);
workInProgress.pendingWorkPriority = OffscreenPriority;
return null;
default:
throw new Error('Unknown unit of work tag');
} else {
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
current ? current.child : null,
nextChildren,
priority
);
workInProgress.pendingWorkPriority = NoWork;
return workInProgress.child;
}
}
}
exports.beginWork = beginWork;
function mountIndeterminateComponent(current, workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.pendingProps;
var value = fn(props);
if (typeof value === 'object' && value && typeof value.render === 'function') {
console.log('performed work on class:', fn.name);
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
if (workInProgress.alternate) {
workInProgress.alternate.tag = ClassComponent;
}
} else {
console.log('performed work on fn:', fn.name);
// Proceed under the assumption that this is a functional component
workInProgress.tag = FunctionalComponent;
if (workInProgress.alternate) {
workInProgress.alternate.tag = FunctionalComponent;
}
}
reconcileChildren(current, workInProgress, value);
workInProgress.pendingWorkPriority = NoWork;
}
function updateCoroutineComponent(current, workInProgress) {
var coroutine = (workInProgress.pendingProps : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
}
console.log('begin coroutine', workInProgress.type.name);
reconcileChildren(current, workInProgress, coroutine.children);
workInProgress.pendingWorkPriority = NoWork;
}
function reuseChildren(returnFiber : Fiber, firstChild : Fiber) {
// TODO: None of this should be necessary if structured better.
// The returnFiber pointer only needs to be updated when we walk into this child
// which we don't do right now. If the pending work priority indicated only
// if a child has work rather than if the node has work, then we would know
// by a single lookup on workInProgress rather than having to go through
// each child.
let child = firstChild;
do {
// Update the returnFiber of the child to the newest fiber.
child.return = returnFiber;
// Retain the priority if there's any work left to do in the children.
if (child.pendingWorkPriority !== NoWork &&
(returnFiber.pendingWorkPriority === NoWork ||
returnFiber.pendingWorkPriority > child.pendingWorkPriority)) {
returnFiber.pendingWorkPriority = child.pendingWorkPriority;
}
} while (child = child.sibling);
}
function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
if (current && workInProgress.pendingProps === current.memoizedProps) {
// The most likely scenario is that the previous copy of the tree contains
// the same props as the new one. In that case, we can just copy the output
// and children from that node.
workInProgress.memoizedProps = workInProgress.pendingProps;
workInProgress.output = current.output;
const priorityLevel = workInProgress.pendingWorkPriority;
workInProgress.pendingProps = null;
workInProgress.pendingWorkPriority = NoWork;
workInProgress.stateNode = current.stateNode;
if (current.child) {
// If we bail out but still has work with the current priority in this
// subtree, we need to go find it right now. If we don't, we won't flush
// it until the next tick.
workInProgress.child = current.child;
reuseChildren(workInProgress, workInProgress.child);
if (workInProgress.pendingWorkPriority <= priorityLevel) {
// TODO: This passes the current node and reads the priority level and
// pending props from that. We want it to read our priority level and
// pending props from the work in progress. Needs restructuring.
return findNextUnitOfWorkAtPriority(workInProgress.alternate, priorityLevel);
} else {
return null;
}
} else {
workInProgress.child = null;
return null;
}
}
if (!workInProgress.hasWorkInProgress &&
workInProgress.pendingProps === workInProgress.memoizedProps &&
workInProgress.pendingWorkPriority === NoWork) {
// If we started this work before, and finished it, or if we're in a
// ping-pong update scenario, this version could already be what we're
// looking for. In that case, we should be able to just bail out.
workInProgress.pendingProps = null;
// TODO: We should be able to bail out if there is remaining work at a lower
// priority too. However, I don't know if that is safe or even better since
// the other tree could've potentially finished that work.
return null;
}
workInProgress.hasWorkInProgress = true;
switch (workInProgress.tag) {
case IndeterminateComponent:
mountIndeterminateComponent(current, workInProgress);
return workInProgress.child;
case FunctionalComponent:
updateFunctionalComponent(current, workInProgress);
return workInProgress.child;
case ClassComponent:
console.log('class component', workInProgress.pendingProps.type.name);
return workInProgress.child;
case HostContainer:
reconcileChildren(current, workInProgress, workInProgress.pendingProps);
// A yield component is just a placeholder, we can just run through the
// next one immediately.
workInProgress.pendingWorkPriority = NoWork;
if (workInProgress.child) {
return beginWork(
workInProgress.child.alternate,
workInProgress.child
);
}
return null;
case HostComponent:
return updateHostComponent(current, workInProgress);
case CoroutineHandlerPhase:
// This is a restart. Reset the tag to the initial phase.
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
updateCoroutineComponent(current, workInProgress);
// This doesn't take arbitrary time so we could synchronously just begin
// eagerly do the work of workInProgress.child as an optimization.
if (workInProgress.child) {
return beginWork(
workInProgress.child.alternate,
workInProgress.child
);
}
return workInProgress.child;
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
workInProgress.pendingWorkPriority = NoWork;
if (workInProgress.sibling) {
return beginWork(
workInProgress.sibling.alternate,
workInProgress.sibling
);
}
return null;
default:
throw new Error('Unknown unit of work tag');
}
}
return {
beginWork,
};
};
@@ -0,0 +1,68 @@
/**
* Copyright 2013-present, 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 ReactFiberCommitWork
* @flow
*/
'use strict';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
ClassComponent,
HostContainer,
HostComponent,
} = ReactTypeOfWork;
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
const updateContainer = config.updateContainer;
const commitUpdate = config.commitUpdate;
function commitWork(finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent: {
// TODO: Fire componentDidMount/componentDidUpdate, update refs
return;
}
case HostContainer: {
// TODO: Attach children to root container.
const children = finishedWork.output;
const root : FiberRoot = finishedWork.stateNode;
const containerInfo : C = root.containerInfo;
updateContainer(containerInfo, children);
return;
}
case HostComponent: {
if (finishedWork.stateNode == null || !finishedWork.alternate) {
throw new Error('This should only be done during updates.');
}
// Commit the work prepared earlier.
const child = (finishedWork.child : ?Fiber);
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const newProps = finishedWork.memoizedProps;
const current = finishedWork.alternate;
const oldProps = current.memoizedProps;
const instance : I = finishedWork.stateNode;
commitUpdate(instance, oldProps, newProps, children);
return;
}
default:
throw new Error('This unit of work tag should not have side-effects.');
}
}
return {
commitWork,
};
};
@@ -14,112 +14,178 @@
import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';
import type { ReifiedYield } from 'ReactReifiedYield';
var ReactChildFiber = require('ReactChildFiber');
var ReactTypesOfWork = require('ReactTypesOfWork');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
IndeterminateComponent,
FunctionalComponent,
ClassComponent,
HostContainer,
HostComponent,
CoroutineComponent,
CoroutineHandlerPhase,
YieldComponent,
} = ReactTypesOfWork;
} = ReactTypeOfWork;
function transferOutput(child : ?Fiber, parent : Fiber) {
// If we have a single result, we just pass that through as the output to
// avoid unnecessary traversal. When we have multiple output, we just pass
// the linked list of fibers that has the individual output values.
parent.output = (child && !child.sibling) ? child.output : child;
parent.memoizedInput = parent.input;
}
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
if (!output) {
// Ignore nulls etc.
} else if (output.tag !== undefined) { // TODO: Fix this fragile duck test.
// Detect if this is a fiber, if so it is a fragment result.
// $FlowFixMe: Refinement issue.
var item = (output : Fiber);
do {
recursivelyFillYields(yields, item.output);
item = item.sibling;
} while (item);
} else {
// $FlowFixMe: Refinement issue. If it is not a Fiber or null, it is a yield
yields.push(output);
}
}
const createInstance = config.createInstance;
const prepareUpdate = config.prepareUpdate;
function moveCoroutineToHandlerPhase(current : ?Fiber, workInProgress : Fiber) {
var coroutine = (workInProgress.input : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
function markForPreEffect(workInProgress : Fiber) {
// Schedule a side-effect on this fiber, BEFORE the children's side-effects.
if (workInProgress.firstEffect) {
workInProgress.nextEffect = workInProgress.firstEffect;
workInProgress.firstEffect = workInProgress;
} else {
workInProgress.firstEffect = workInProgress;
workInProgress.lastEffect = workInProgress;
}
}
// First step of the coroutine has completed. Now we need to do the second.
// TODO: It would be nice to have a multi stage coroutine represented by a
// single component, or at least tail call optimize nested ones. Currently
// that requires additional fields that we don't want to add to the fiber.
// So this requires nested handlers.
// Note: This doesn't mutate the alternate node. I don't think it needs to
// since this stage is reset for every pass.
workInProgress.tag = CoroutineHandlerPhase;
// Build up the yields.
// TODO: Compare this to a generator or opaque helpers like Children.
var yields : Array<ReifiedYield> = [];
var child = workInProgress.child;
while (child) {
recursivelyFillYields(yields, child.output);
child = child.sibling;
/*
function markForPostEffect(workInProgress : Fiber) {
// Schedule a side-effect on this fiber, AFTER the children's side-effects.
if (workInProgress.lastEffect) {
workInProgress.lastEffect.nextEffect = workInProgress;
} else {
workInProgress.firstEffect = workInProgress;
}
workInProgress.lastEffect = workInProgress;
}
var fn = coroutine.handler;
var props = coroutine.props;
var nextChildren = fn(props, yields);
*/
var currentFirstChild = current ? current.stateNode : null;
workInProgress.stateNode = ReactChildFiber.reconcileChildFibers(
workInProgress,
currentFirstChild,
nextChildren
);
return workInProgress.stateNode;
}
exports.completeWork = function(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
switch (workInProgress.tag) {
case FunctionalComponent:
console.log('/functional component', workInProgress.type.name);
transferOutput(workInProgress.child, workInProgress);
break;
case ClassComponent:
console.log('/class component', workInProgress.type.name);
transferOutput(workInProgress.child, workInProgress);
break;
case HostComponent:
console.log('/host component', workInProgress.type);
break;
case CoroutineComponent:
console.log('/coroutine component', workInProgress.input.handler.name);
return moveCoroutineToHandlerPhase(current, workInProgress);
case CoroutineHandlerPhase:
transferOutput(workInProgress.stateNode, workInProgress);
// Reset the tag to now be a first phase coroutine.
workInProgress.tag = CoroutineComponent;
break;
case YieldComponent:
// Does nothing.
break;
// Error cases
case IndeterminateComponent:
throw new Error('An indeterminate component should have become determinate before completing.');
default:
throw new Error('Unknown unit of work tag');
function transferOutput(child : ?Fiber, returnFiber : Fiber) {
// If we have a single result, we just pass that through as the output to
// avoid unnecessary traversal. When we have multiple output, we just pass
// the linked list of fibers that has the individual output values.
returnFiber.output = (child && !child.sibling) ? child.output : child;
returnFiber.memoizedProps = returnFiber.pendingProps;
}
return null;
function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
if (!output) {
// Ignore nulls etc.
} else if (output.tag !== undefined) { // TODO: Fix this fragile duck test.
// Detect if this is a fiber, if so it is a fragment result.
// $FlowFixMe: Refinement issue.
var item = (output : Fiber);
do {
recursivelyFillYields(yields, item.output);
item = item.sibling;
} while (item);
} else {
// $FlowFixMe: Refinement issue. If it is not a Fiber or null, it is a yield
yields.push(output);
}
}
function moveCoroutineToHandlerPhase(current : ?Fiber, workInProgress : Fiber) {
var coroutine = (workInProgress.pendingProps : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
}
// First step of the coroutine has completed. Now we need to do the second.
// TODO: It would be nice to have a multi stage coroutine represented by a
// single component, or at least tail call optimize nested ones. Currently
// that requires additional fields that we don't want to add to the fiber.
// So this requires nested handlers.
// Note: This doesn't mutate the alternate node. I don't think it needs to
// since this stage is reset for every pass.
workInProgress.tag = CoroutineHandlerPhase;
// Build up the yields.
// TODO: Compare this to a generator or opaque helpers like Children.
var yields : Array<ReifiedYield> = [];
var child = workInProgress.child;
while (child) {
recursivelyFillYields(yields, child.output);
child = child.sibling;
}
var fn = coroutine.handler;
var props = coroutine.props;
var nextChildren = fn(props, yields);
var currentFirstChild = current ? current.stateNode : null;
// Inherit the priority of the returnFiber.
const priority = workInProgress.pendingWorkPriority;
workInProgress.stateNode = ReactChildFiber.reconcileChildFibers(
workInProgress,
currentFirstChild,
nextChildren,
priority
);
return workInProgress.stateNode;
}
function completeWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
switch (workInProgress.tag) {
case FunctionalComponent:
console.log('/functional component', workInProgress.type.name);
transferOutput(workInProgress.child, workInProgress);
return null;
case ClassComponent:
console.log('/class component', workInProgress.type.name);
transferOutput(workInProgress.child, workInProgress);
return null;
case HostContainer:
transferOutput(workInProgress.child, workInProgress);
// We don't know if a container has updated any children so we always
// need to update it right now. We schedule this side-effect before
// all the other side-effects in the subtree. We need to schedule it
// before so that the entire tree is up-to-date before the life-cycles
// are invoked.
markForPreEffect(workInProgress);
return null;
case HostComponent:
console.log('/host component', workInProgress.type);
const child = workInProgress.child;
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const newProps = workInProgress.pendingProps;
workInProgress.memoizedProps = newProps;
if (current && workInProgress.stateNode != null) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
const oldProps = current.memoizedProps;
const instance : I = workInProgress.stateNode;
if (prepareUpdate(instance, oldProps, newProps, children)) {
// This returns true if there was something to update.
markForPreEffect(workInProgress);
}
workInProgress.output = instance;
} else {
const instance = createInstance(workInProgress.type, newProps, children);
// TODO: This seems like unnecessary duplication.
workInProgress.stateNode = instance;
workInProgress.output = instance;
}
return null;
case CoroutineComponent:
console.log('/coroutine component', workInProgress.pendingProps.handler.name);
return moveCoroutineToHandlerPhase(current, workInProgress);
case CoroutineHandlerPhase:
transferOutput(workInProgress.stateNode, workInProgress);
// Reset the tag to now be a first phase coroutine.
workInProgress.tag = CoroutineComponent;
return null;
case YieldComponent:
// Does nothing.
return null;
// Error cases
case IndeterminateComponent:
throw new Error('An indeterminate component should have become determinate before completing.');
default:
throw new Error('Unknown unit of work tag');
}
}
return {
completeWork,
};
};
@@ -0,0 +1,93 @@
/**
* Copyright 2013-present, 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 ReactFiberPendingWork
* @flow
*/
'use strict';
import type { Fiber } from 'ReactFiber';
import type { PriorityLevel } from 'ReactPriorityLevel';
var ReactFiber = require('ReactFiber');
var {
NoWork,
} = require('ReactPriorityLevel');
function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fiber) {
while (current.sibling) {
current = current.sibling;
workInProgress = workInProgress.sibling = ReactFiber.cloneFiber(
current,
current.pendingWorkPriority
);
workInProgress.return = returnFiber;
}
workInProgress.sibling = null;
}
exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
let current = currentRoot;
while (current) {
if (current.pendingWorkPriority !== NoWork &&
current.pendingWorkPriority <= priorityLevel) {
// This node has work to do that fits our priority level criteria.
if (current.pendingProps !== null) {
// We found some work to do. We need to return the "work in progress"
// of this node which will be the alternate.
const workInProgress = current.alternate;
if (!workInProgress) {
throw new Error('Should have wip now');
}
workInProgress.pendingProps = current.pendingProps;
return workInProgress;
}
// If we have a child let's see if any of our children has work to do.
// Only bother doing this at all if the current priority level matches
// because it is the highest priority for the whole subtree.
// TODO: Coroutines can have work in their stateNode which is another
// type of child that needs to be searched for work.
if (current.child) {
// Ensure we have a work in progress copy to backtrack through.
let currentChild = current.child;
let workInProgress = current.alternate;
if (!workInProgress) {
throw new Error('Should have wip now');
}
workInProgress.pendingWorkPriority = current.pendingWorkPriority;
workInProgress.child = ReactFiber.cloneFiber(currentChild, NoWork);
workInProgress.child.return = workInProgress;
cloneSiblings(currentChild, workInProgress.child, workInProgress);
current = current.child;
continue;
}
// If we match the priority but has no child and no work to do,
// then we can safely reset the flag.
current.pendingWorkPriority = NoWork;
}
if (current === currentRoot) {
return null;
}
while (!current.sibling) {
current = current.return;
if (!current) {
return null;
}
if (current.pendingWorkPriority <= priorityLevel) {
// If this subtree had work left to do, we would have returned it by
// now. This could happen if a child with pending work gets cleaned up
// but we don't clear the flag then. It is safe to reset it now.
current.pendingWorkPriority = NoWork;
}
}
current = current.sibling;
}
return null;
};
@@ -13,135 +13,98 @@
'use strict';
import type { Fiber } from 'ReactFiber';
var ReactFiber = require('ReactFiber');
var { beginWork } = require('ReactFiberBeginWork');
var { completeWork } = require('ReactFiberCompleteWork');
import type { FiberRoot } from 'ReactFiberRoot';
import type { TypeOfWork } from 'ReactTypeOfWork';
type ReactHostElement<T, P> = {
type: T,
props: P
};
var { createFiberRoot } = require('ReactFiberRoot');
var ReactFiberScheduler = require('ReactFiberScheduler');
var {
LowPriority,
} = require('ReactPriorityLevel');
type Deadline = {
timeRemaining : () => number
};
var timeHeuristicForUnitOfWork = 1;
type HostChildNode<I> = { tag: TypeOfWork, output: HostChildren<I>, sibling: any };
export type HostConfig<T, P, I> = {
export type HostChildren<I> = null | void | I | HostChildNode<I>;
export type HostConfig<T, P, I, C> = {
// TODO: We don't currently have a quick way to detect that children didn't
// reorder so we host will always need to check the set. We should make a flag
// or something so that it can bailout easily.
updateContainer(containerInfo : C, children : HostChildren<I>) : void;
createInstance(type : T, props : P, children : HostChildren<I>) : I,
prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I>) : bool,
commitUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I>) : void,
deleteInstance(instance : I) : void,
createHostInstance(element : ReactHostElement<T, P>) : I,
scheduleHighPriCallback(callback : () => void) : void,
scheduleLowPriCallback(callback : (deadline : Deadline) => void) : void
};
type OpaqueID = {};
type OpaqueNode = Fiber;
export type Reconciler = {
mountNewRoot(element : ReactElement<any>) : OpaqueID;
export type Reconciler<C> = {
mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode,
updateContainer(element : ReactElement<any>, container : OpaqueNode) : void,
unmountContainer(container : OpaqueNode) : void,
// Used to extract the return value from the initial render. Legacy API.
getPublicRootInstance(container : OpaqueNode) : (C | null),
};
module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconciler<C> {
// const scheduleHighPriCallback = config.scheduleHighPriCallback;
const scheduleLowPriCallback = config.scheduleLowPriCallback;
let nextUnitOfWork : ?Fiber = null;
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
while (true) {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
const next = completeWork(current, workInProgress);
if (next) {
// If completing this work spawned new work, do that next.
return next;
} else if (workInProgress.sibling) {
// If there is more work to do in this parent, do that next.
return workInProgress.sibling;
} else if (workInProgress.parent) {
// If there's no more work in this parent. Complete the parent.
// TODO: Stop using the parent for this purpose. I think this will break
// down in edge cases because when nodes are reused during bailouts, we
// don't know which of two parents was used. Instead we should maintain
// a temporary manual stack.
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
workInProgress = workInProgress.parent;
} else {
// If we're at the root, there's no more work to do.
return null;
}
}
}
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
const next = beginWork(current, workInProgress);
if (next) {
// If this spawns new work, do that next.
return next;
} else {
// Otherwise, complete the current work.
return completeUnitOfWork(workInProgress);
}
}
function performLowPriWork(deadline : Deadline) {
while (nextUnitOfWork) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
} else {
scheduleLowPriCallback(performLowPriWork);
break;
}
}
}
function ensureLowPriIsScheduled() {
if (nextUnitOfWork) {
return;
}
scheduleLowPriCallback(performLowPriWork);
}
/*
function performHighPriWork() {
// There is no such thing as high pri work yet.
}
function ensureHighPriIsScheduled() {
scheduleHighPriCallback(performHighPriWork);
}
*/
let rootFiber : ?Fiber = null;
var { scheduleLowPriWork } = ReactFiberScheduler(config);
return {
mountNewRoot(element : ReactElement<any>) : OpaqueID {
mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode {
const root = createFiberRoot(containerInfo);
const container = root.current;
// TODO: Use pending work/state instead of props.
container.pendingProps = element;
container.pendingWorkPriority = LowPriority;
ensureLowPriIsScheduled();
scheduleLowPriWork(root);
// TODO: Unify this with ReactChildFiber. We can't now because the parent
// is passed. Should be doable though. Might require a wrapper don't know.
if (rootFiber && rootFiber.type === element.type && rootFiber.key === element.key) {
nextUnitOfWork = ReactFiber.cloneFiber(rootFiber);
nextUnitOfWork.input = element.props;
return {};
}
// It may seem strange that we don't return the root here, but that will
// allow us to have containers that are in the middle of the tree instead
// of being roots.
return container;
},
nextUnitOfWork = rootFiber = ReactFiber.createFiberFromElement(element);
updateContainer(element : ReactElement<any>, container : OpaqueNode) : void {
// TODO: If this is a nested container, this won't be the root.
const root : FiberRoot = (container.stateNode : any);
// TODO: Use pending work/state instead of props.
root.current.pendingProps = element;
root.current.pendingWorkPriority = LowPriority;
return {};
scheduleLowPriWork(root);
},
unmountContainer(container : OpaqueNode) : void {
// TODO: If this is a nested container, this won't be the root.
const root : FiberRoot = (container.stateNode : any);
// TODO: Use pending work/state instead of props.
root.current.pendingProps = [];
root.current.pendingWorkPriority = LowPriority;
scheduleLowPriWork(root);
},
getPublicRootInstance(container : OpaqueNode) : (C | null) {
return null;
},
};
};
@@ -0,0 +1,42 @@
/**
* Copyright 2013-present, 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 ReactFiberRoot
* @flow
*/
'use strict';
import type { Fiber } from 'ReactFiber';
const { createHostContainerFiber } = require('ReactFiber');
export type FiberRoot = {
// Any additional information from the host associated with this root.
containerInfo: any,
// The currently active root fiber. This is the mutable root of the tree.
current: Fiber,
// Determines if this root has already been added to the schedule for work.
isScheduled: bool,
// The work schedule is a linked list.
nextScheduledRoot: ?FiberRoot,
};
exports.createFiberRoot = function(containerInfo : any) : FiberRoot {
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostContainerFiber();
const root = {
current: uninitializedFiber,
containerInfo: containerInfo,
isScheduled: false,
nextScheduledRoot: null,
};
uninitializedFiber.stateNode = root;
return root;
};
@@ -0,0 +1,248 @@
/**
* Copyright 2013-present, 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 ReactFiberScheduler
* @flow
*/
'use strict';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
var ReactFiberBeginWork = require('ReactFiberBeginWork');
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
var ReactFiberCommitWork = require('ReactFiberCommitWork');
var { cloneFiber } = require('ReactFiber');
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
var {
NoWork,
HighPriority,
LowPriority,
OffscreenPriority,
} = require('ReactPriorityLevel');
var timeHeuristicForUnitOfWork = 1;
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
const { beginWork } = ReactFiberBeginWork(config);
const { completeWork } = ReactFiberCompleteWork(config);
const { commitWork } = ReactFiberCommitWork(config);
// const scheduleHighPriCallback = config.scheduleHighPriCallback;
const scheduleLowPriCallback = config.scheduleLowPriCallback;
// The next work in progress fiber that we're currently working on.
let nextUnitOfWork : ?Fiber = null;
// Linked list of roots with scheduled work on them.
let nextScheduledRoot : ?FiberRoot = null;
let lastScheduledRoot : ?FiberRoot = null;
function findNextUnitOfWork() {
// Clear out roots with no more work on them.
while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
nextScheduledRoot.isScheduled = false;
if (nextScheduledRoot === lastScheduledRoot) {
nextScheduledRoot = null;
lastScheduledRoot = null;
return null;
}
nextScheduledRoot = nextScheduledRoot.nextScheduledRoot;
}
let root = nextScheduledRoot;
while (root) {
cloneFiber(root.current, root.current.pendingWorkPriority);
// Find the highest possible priority work to do.
// This loop is unrolled just to satisfy Flow's enum constraint.
// We could make arbitrary many idle priority levels but having
// too many just means flushing changes too often.
let work = findNextUnitOfWorkAtPriority(root.current, HighPriority);
if (work) {
return work;
}
work = findNextUnitOfWorkAtPriority(root.current, LowPriority);
if (work) {
return work;
}
work = findNextUnitOfWorkAtPriority(root.current, OffscreenPriority);
if (work) {
return work;
}
// We didn't find anything to do in this root, so let's try the next one.
root = root.nextScheduledRoot;
}
return null;
}
function commitAllWork(finishedWork : Fiber) {
// Commit all the side-effects within a tree.
// TODO: Error handling.
let effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
commitWork(effectfulFiber);
const next = effectfulFiber.nextEffect;
// Ensure that we clean these up so that we don't accidentally keep them.
// I'm not actually sure this matters because we can't reset firstEffect
// and lastEffect since they're on every node, not just the effectful
// ones. So we have to clean everything as we reuse nodes anyway.
effectfulFiber.nextEffect = null;
effectfulFiber = next;
}
}
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
while (true) {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
const next = completeWork(current, workInProgress);
// The work is now done. We don't need this anymore. This flags
// to the system not to redo any work here.
workInProgress.pendingProps = null;
if (workInProgress.pendingWorkPriority === NoWork) {
workInProgress.hasWorkInProgress = false;
}
const returnFiber = workInProgress.return;
if (returnFiber) {
// Ensure that remaining work priority bubbles up.
if (workInProgress.pendingWorkPriority !== NoWork &&
(returnFiber.pendingWorkPriority === NoWork ||
returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) {
returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority;
}
// Ensure that the first and last effect of the parent corresponds
// to the children's first and last effect. This probably relies on
// children completing in order.
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
}
if (next) {
// If completing this work spawned new work, do that next.
return next;
} else if (workInProgress.sibling) {
// If there is more work to do in this returnFiber, do that next.
return workInProgress.sibling;
} else if (returnFiber) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
} else {
// If we're at the root, there's no more work to do. We can flush it.
const root : FiberRoot = (workInProgress.stateNode : any);
root.current = workInProgress;
// TODO: We can be smarter here and only look for more work in the
// "next" scheduled work since we've already scanned passed. That
// also ensures that work scheduled during reconciliation gets deferred.
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
console.log('----- COMPLETED with remaining work:', workInProgress.pendingWorkPriority);
commitAllWork(workInProgress);
const nextWork = findNextUnitOfWork();
// if (!nextWork && hasMoreWork) {
// TODO: This can happen when some deep work completes and we don't
// know if this was the last one. We should be able to keep track of
// the highest priority still in the tree for one pass. But if we
// terminate an update we don't know.
// throw new Error('FiberRoots should not have flagged more work if there is none.');
// }
return nextWork;
}
}
}
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
// Ignore work if there is nothing to do.
if (workInProgress.pendingProps === null) {
return completeUnitOfWork(workInProgress);
}
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
const next = beginWork(current, workInProgress);
if (next) {
// If this spawns new work, do that next.
return next;
} else {
// Otherwise, complete the current work.
return completeUnitOfWork(workInProgress);
}
}
function performLowPriWork(deadline) {
if (!nextUnitOfWork) {
nextUnitOfWork = findNextUnitOfWork();
}
while (nextUnitOfWork) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
if (!nextUnitOfWork) {
// Find more work. We might have time to complete some more.
nextUnitOfWork = findNextUnitOfWork();
}
} else {
scheduleLowPriCallback(performLowPriWork);
return;
}
}
}
function scheduleLowPriWork(root : FiberRoot) {
// We must reset the current unit of work pointer so that we restart the
// search from the root during the next tick, in case there is now higher
// priority work somewhere earlier than before.
nextUnitOfWork = null;
if (root.isScheduled) {
// If we're already scheduled, we can bail out.
return;
}
root.isScheduled = true;
if (lastScheduledRoot) {
// Schedule ourselves to the end.
lastScheduledRoot.nextScheduledRoot = root;
lastScheduledRoot = root;
} else {
// We're the only work scheduled.
nextScheduledRoot = root;
lastScheduledRoot = root;
scheduleLowPriCallback(performLowPriWork);
}
}
/*
function performHighPriWork() {
// There is no such thing as high pri work yet.
}
function ensureHighPriIsScheduled() {
scheduleHighPriCallback(performHighPriWork);
}
*/
return {
scheduleLowPriWork: scheduleLowPriWork,
};
};
@@ -0,0 +1,24 @@
/**
* Copyright 2013-present, 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 ReactPriorityLevel
* @flow
*/
'use strict';
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
module.exports = {
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
AnimationPriority: 2, // Needs to complete before the next frame.
HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 4, // Data fetching, or result from updating stores.
OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};
@@ -6,20 +6,21 @@
* 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 ReactTypesOfWork
* @providesModule ReactTypeOfWork
* @flow
*/
'use strict';
var TypesOfWork = {
export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
module.exports = {
IndeterminateComponent: 0, // Before we know whether it is functional or class
FunctionalComponent: 1,
ClassComponent: 2,
HostComponent: 3,
CoroutineComponent: 4,
CoroutineHandlerPhase: 5,
YieldComponent: 6,
HostContainer: 3, // Root of a host tree. Could be nested inside another node.
HostComponent: 4,
CoroutineComponent: 5,
CoroutineHandlerPhase: 6,
YieldComponent: 7,
};
module.exports = TypesOfWork;
@@ -109,10 +109,332 @@ describe('ReactIncremental', function() {
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flush();
// TODO: Test bail out of host components. This is currently unobservable.
// Since this is an update, it should bail out and reuse the work from
// Header and Content.
expect(ops).toEqual(['Foo', 'Content']);
});
it('can cancel partially rendered work and restart', function() {
var ops = [];
function Bar(props) {
ops.push('Bar');
return <div>{props.children}</div>;
}
function Foo(props) {
ops.push('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<Bar>{props.text}</Bar>
</div>
);
}
// Init
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flush();
ops = [];
ReactNoop.render(<Foo text="bar" />);
// Flush part of the work
ReactNoop.flushLowPri(20);
expect(ops).toEqual(['Foo', 'Bar']);
ops = [];
// This will abort the previous work and restart
ReactNoop.render(<Foo text="baz" />);
// Flush part of the new work
ReactNoop.flushLowPri(20);
expect(ops).toEqual(['Foo', 'Bar']);
// Flush the rest of the work which now includes the low priority
ReactNoop.flush(20);
expect(ops).toEqual(['Foo', 'Bar', 'Bar']);
});
it('can deprioritize unfinished work and resume it later', function() {
var ops = [];
function Bar(props) {
ops.push('Bar');
return <div>{props.children}</div>;
}
function Middle(props) {
ops.push('Middle');
return <span>{props.children}</span>;
}
function Foo(props) {
ops.push('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<section hidden={true}>
<Middle>{props.text}</Middle>
</section>
<Bar>{props.text}</Bar>
<footer hidden={true}>
<Middle>Footer</Middle>
</footer>
</div>
);
}
// Init
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flush();
expect(ops).toEqual(['Foo', 'Bar', 'Bar', 'Middle', 'Middle']);
ops = [];
// Render part of the work. This should be enough to flush everything except
// the middle which has lower priority.
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flushLowPri(40);
expect(ops).toEqual(['Foo', 'Bar', 'Bar']);
ops = [];
// Flush only the remaining work
ReactNoop.flush();
expect(ops).toEqual(['Middle', 'Middle']);
});
it('can resume work in a subtree even when a parent bails out', function() {
var ops = [];
function Bar(props) {
ops.push('Bar');
return <div>{props.children}</div>;
}
function Tester() {
// This component is just here to ensure that the bail out is
// in fact in effect in the expected place for this test.
ops.push('Tester');
return <div />;
}
function Middle(props) {
ops.push('Middle');
return <span>{props.children}</span>;
}
var middleContent = (
<aaa>
<Tester />
<bbb hidden={true}>
<ccc>
<Middle>Hi</Middle>
</ccc>
</bbb>
</aaa>
);
function Foo(props) {
ops.push('Foo');
return (
<div>
<Bar>{props.text}</Bar>
{middleContent}
<Bar>{props.text}</Bar>
</div>
);
}
// Init
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flushLowPri(52);
expect(ops).toEqual(['Foo', 'Bar', 'Tester', 'Bar']);
ops = [];
// We're now rendering an update that will bail out on updating middle.
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flushLowPri(45);
expect(ops).toEqual(['Foo', 'Bar', 'Bar']);
ops = [];
// Flush the rest to make sure that the bailout didn't block this work.
ReactNoop.flush();
expect(ops).toEqual(['Middle']);
});
it('can resume work in a bailed subtree within one pass', function() {
var ops = [];
function Bar(props) {
ops.push('Bar');
return <div>{props.children}</div>;
}
function Tester() {
// This component is just here to ensure that the bail out is
// in fact in effect in the expected place for this test.
ops.push('Tester');
return <div />;
}
function Middle(props) {
ops.push('Middle');
return <span>{props.children}</span>;
}
var middleContent = (
<aaa>
<Tester />
<bbb hidden={true}>
<ccc>
<Middle>Hi</Middle>
</ccc>
</bbb>
</aaa>
);
function Foo(props) {
ops.push('Foo');
return (
<div hidden={props.text === 'bar'}>
<Bar>{props.text}</Bar>
{middleContent}
<Bar>{props.text}</Bar>
</div>
);
}
// Init
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flushLowPri(52);
expect(ops).toEqual(['Foo', 'Bar', 'Tester', 'Bar']);
ops = [];
// Make a quick update which will create a low pri tree on top of the
// already low pri tree.
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flushLowPri(15);
expect(ops).toEqual(['Foo']);
ops = [];
// At this point, middle will bail out but it has not yet fully rendered.
// Since that is the same priority as its parent tree. This should render
// as a single batch. Therefore, it is correct that Middle should be in the
// middle. If it occurs after the two "Bar" components then it was flushed
// after them which is not correct.
ReactNoop.flush();
expect(ops).toEqual(['Bar', 'Middle', 'Bar']);
});
it('can reuse work done after being preempted', function() {
var ops = [];
function Bar(props) {
ops.push('Bar');
return <div>{props.children}</div>;
}
function Middle(props) {
ops.push('Middle');
return <span>{props.children}</span>;
}
var middleContent = (
<div>
<Middle>Hello</Middle>
<Bar>-</Bar>
<Middle>World</Middle>
</div>
);
function Foo(props) {
ops.push('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<div hidden={true}>
{
props.step === 0 ?
<div>
<Middle>Hi</Middle>
<Bar>{props.text}</Bar>
<Middle>There</Middle>
</div>
: middleContent
}
</div>
</div>
);
}
// Start rendering an update
// Init
ReactNoop.render(<Foo text="foo" step={0} />);
ReactNoop.flush();
expect(ops).toEqual(['Foo', 'Bar', 'Middle', 'Bar', 'Middle']);
ops = [];
// Make a quick update which will schedule low priority work to
// update the middle content.
ReactNoop.render(<Foo text="bar" step={1} />);
ReactNoop.flushLowPri(30);
expect(ops).toEqual(['Foo', 'Bar']);
ops = [];
// The middle content is now pending rendering...
ReactNoop.flushLowPri(30);
expect(ops).toEqual(['Middle', 'Bar']);
ops = [];
// but we'll interupt it to render some higher priority work.
// The middle content will bailout so it remains untouched.
ReactNoop.render(<Foo text="foo" step={1} />);
ReactNoop.flushLowPri(30);
expect(ops).toEqual(['Foo', 'Bar']);
ops = [];
// Since we did nothing to the middle subtree during the interuption,
// we should be able to reuse the reconciliation work that we already did
// without restarting.
ReactNoop.flush();
expect(ops).toEqual(['Middle']);
});
});
@@ -0,0 +1,97 @@
/**
* Copyright 2013-present, 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;
var ReactNoop;
describe('ReactIncremental', function() {
beforeEach(function() {
React = require('React');
ReactNoop = require('ReactNoop');
spyOn(console, 'log');
});
function div(...children) {
return { type: 'div', children };
}
function span(...children) {
return { type: 'span', children };
}
it('can update child nodes of a host instance', function() {
function Bar(props) {
return <span>{props.text}</span>;
}
function Foo(props) {
return (
<div>
<Bar text={props.text} />
{props.text === 'World' ? <Bar text={props.text} /> : null}
</div>
);
}
ReactNoop.render(<Foo text="Hello" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(span()),
]);
ReactNoop.render(<Foo text="World" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(span(), span()),
]);
});
it('does not update child nodes if a flush is aborted', function() {
function Bar(props) {
return <span>{props.text}</span>;
}
function Foo(props) {
return (
<div>
<div>
<Bar text={props.text} />
{props.text === 'Hello' ? <Bar text={props.text} /> : null}
</div>
<Bar text="Yo" />
</div>
);
}
ReactNoop.render(<Foo text="Hello" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(div(span(), span()), span()),
]);
ReactNoop.render(<Foo text="World" />);
ReactNoop.flushLowPri(35);
expect(ReactNoop.root.children).toEqual([
div(div(span(), span()), span()),
]);
});
// TODO: Test that side-effects are not cut off when a work in progress node
// moves to "current" without flushing due to having lower priority. Does this
// even happen? Maybe a child doesn't get processed because it is lower prio?
});
@@ -496,7 +496,7 @@ var ResponderEventPlugin = {
);
}
ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent, nativeEventTarget);
ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
var extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent) ?
setResponderAndExtractTransfer(
@@ -7,37 +7,44 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ResponderTouchHistoryStore
* @flow
*/
'use strict';
var EventPluginUtils = require('EventPluginUtils');
const EventPluginUtils = require('EventPluginUtils');
var invariant = require('invariant');
const invariant = require('invariant');
const warning = require('warning');
var isMoveish = EventPluginUtils.isMoveish;
var isStartish = EventPluginUtils.isStartish;
var isEndish = EventPluginUtils.isEndish;
var MAX_TOUCH_BANK = 20;
const {
isEndish,
isMoveish,
isStartish,
} = EventPluginUtils;
/**
* Touch position/time tracking information by touchID. Typically, we'll only
* see IDs with a range of 1-20 (they are recycled when touches end and then
* start again). This data is commonly needed by many different interaction
* logic modules so precomputing it is very helpful to do once.
* Each touch object in `touchBank` is of the following form:
* { touchActive: boolean,
* startTimeStamp: number,
* startPageX: number,
* startPageY: number,
* currentPageX: number,
* currentPageY: number,
* currentTimeStamp: number
* }
* Tracks the position and time of each active touch by `touch.identifier`. We
* should typically only see IDs in the range of 1-20 because IDs get recycled
* when touches end and start again.
*/
var touchHistory = {
touchBank: [],
type TouchRecord = {
touchActive: boolean,
startPageX: number,
startPageY: number,
startTimeStamp: number,
currentPageX: number,
currentPageY: number,
currentTimeStamp: number,
previousPageX: number,
previousPageY: number,
previousTimeStamp: number,
};
const MAX_TOUCH_BANK = 20;
const touchBank: Array<TouchRecord> = [];
const touchHistory = {
touchBank,
numberActiveTouches: 0,
// If there is only one active touch, we remember its location. This prevents
// us having to loop through all of the touches all the time in the most
@@ -46,24 +53,34 @@ var touchHistory = {
mostRecentTimeStamp: 0,
};
var timestampForTouch = function(touch) {
type Touch = {
identifier: ?number,
pageX: number,
pageY: number,
timestamp: number,
};
type TouchEvent = {
changedTouches: Array<Touch>,
touches: Array<Touch>,
};
function timestampForTouch(touch: Touch): number {
// The legacy internal implementation provides "timeStamp", which has been
// renamed to "timestamp". Let both work for now while we iron it out
// TODO (evv): rename timeStamp to timestamp in internal code
return touch.timeStamp || touch.timestamp;
};
return (touch: any).timeStamp || touch.timestamp;
}
/**
* TODO: Instead of making gestures recompute filtered velocity, we could
* include a built in velocity computation that can be reused globally.
* @param {Touch} touch Native touch object.
*/
var initializeTouchData = function(touch) {
function createTouchRecord(touch: Touch): TouchRecord {
return {
touchActive: true,
startTimeStamp: timestampForTouch(touch),
startPageX: touch.pageX,
startPageY: touch.pageY,
startTimeStamp: timestampForTouch(touch),
currentPageX: touch.pageX,
currentPageY: touch.pageY,
currentTimeStamp: timestampForTouch(touch),
@@ -71,114 +88,140 @@ var initializeTouchData = function(touch) {
previousPageY: touch.pageY,
previousTimeStamp: timestampForTouch(touch),
};
};
}
var reinitializeTouchTrack = function(touchTrack, touch) {
touchTrack.touchActive = true;
touchTrack.startTimeStamp = timestampForTouch(touch);
touchTrack.startPageX = touch.pageX;
touchTrack.startPageY = touch.pageY;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.previousPageX = touch.pageX;
touchTrack.previousPageY = touch.pageY;
touchTrack.previousTimeStamp = timestampForTouch(touch);
};
function resetTouchRecord(touchRecord: TouchRecord, touch: Touch): void {
touchRecord.touchActive = true;
touchRecord.startPageX = touch.pageX;
touchRecord.startPageY = touch.pageY;
touchRecord.startTimeStamp = timestampForTouch(touch);
touchRecord.currentPageX = touch.pageX;
touchRecord.currentPageY = touch.pageY;
touchRecord.currentTimeStamp = timestampForTouch(touch);
touchRecord.previousPageX = touch.pageX;
touchRecord.previousPageY = touch.pageY;
touchRecord.previousTimeStamp = timestampForTouch(touch);
}
var validateTouch = function(touch) {
var identifier = touch.identifier;
invariant(identifier != null, 'Touch object is missing identifier');
if (identifier > MAX_TOUCH_BANK) {
console.warn(
'Touch identifier ' + identifier + ' is greater than maximum ' +
'supported ' + MAX_TOUCH_BANK + ' which causes performance issues ' +
'backfilling array locations for all of the indices.'
function getTouchIdentifier({identifier}: Touch): number {
invariant(identifier != null, 'Touch object is missing identifier.');
warning(
identifier <= MAX_TOUCH_BANK,
'Touch identifier %s is greater than maximum supported %s which causes ' +
'performance issues backfilling array locations for all of the indices.',
identifier,
MAX_TOUCH_BANK
);
return identifier;
}
function recordTouchStart(touch: Touch): void {
const identifier = getTouchIdentifier(touch);
const touchRecord = touchBank[identifier];
if (touchRecord) {
resetTouchRecord(touchRecord, touch);
} else {
touchBank[identifier] = createTouchRecord(touch);
}
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
}
function recordTouchMove(touch: Touch): void {
const touchRecord = touchBank[getTouchIdentifier(touch)];
if (touchRecord) {
touchRecord.touchActive = true;
touchRecord.previousPageX = touchRecord.currentPageX;
touchRecord.previousPageY = touchRecord.currentPageY;
touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
touchRecord.currentPageX = touch.pageX;
touchRecord.currentPageY = touch.pageY;
touchRecord.currentTimeStamp = timestampForTouch(touch);
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
} else {
console.error(
'Cannot record touch move without a touch start.\n' +
'Touch Move: %s\n',
'Touch Bank: %s',
printTouch(touch),
printTouchBank()
);
}
};
}
var recordStartTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var identifier = touch.identifier;
var touchTrack = touchBank[identifier];
if (__DEV__) {
validateTouch(touch);
}
if (touchTrack) {
reinitializeTouchTrack(touchTrack, touch);
function recordTouchEnd(touch: Touch): void {
const touchRecord = touchBank[getTouchIdentifier(touch)];
if (touchRecord) {
touchRecord.touchActive = false;
touchRecord.previousPageX = touchRecord.currentPageX;
touchRecord.previousPageY = touchRecord.currentPageY;
touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
touchRecord.currentPageX = touch.pageX;
touchRecord.currentPageY = touch.pageY;
touchRecord.currentTimeStamp = timestampForTouch(touch);
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
} else {
touchBank[touch.identifier] = initializeTouchData(touch);
console.error(
'Cannot record touch end without a touch start.\n' +
'Touch End: %s\n',
'Touch Bank: %s',
printTouch(touch),
printTouchBank()
);
}
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
}
var recordMoveTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var touchTrack = touchBank[touch.identifier];
if (__DEV__) {
validateTouch(touch);
invariant(touchTrack, 'Touch data should have been recorded on start');
function printTouch(touch: Touch): string {
return JSON.stringify({
identifier: touch.identifier,
pageX: touch.pageX,
pageY: touch.pageY,
timestamp: timestampForTouch(touch),
});
}
function printTouchBank(): string {
let printed = JSON.stringify(touchBank.slice(0, MAX_TOUCH_BANK));
if (touchBank.length > MAX_TOUCH_BANK) {
printed += ' (original size: ' + touchBank.length + ')';
}
touchTrack.touchActive = true;
touchTrack.previousPageX = touchTrack.currentPageX;
touchTrack.previousPageY = touchTrack.currentPageY;
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
return printed;
}
var recordEndTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var touchTrack = touchBank[touch.identifier];
if (__DEV__) {
validateTouch(touch);
invariant(touchTrack, 'Touch data should have been recorded on start');
}
touchTrack.previousPageX = touchTrack.currentPageX;
touchTrack.previousPageY = touchTrack.currentPageY;
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.touchActive = false;
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var ResponderTouchHistoryStore = {
recordTouchTrack: function(topLevelType, nativeEvent) {
var touchBank = touchHistory.touchBank;
const ResponderTouchHistoryStore = {
recordTouchTrack(topLevelType: string, nativeEvent: TouchEvent): void {
if (isMoveish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordMoveTouchData);
nativeEvent.changedTouches.forEach(recordTouchMove);
} else if (isStartish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordStartTouchData);
nativeEvent.changedTouches.forEach(recordTouchStart);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
touchHistory.indexOfSingleActiveTouch = nativeEvent.touches[0].identifier;
touchHistory.indexOfSingleActiveTouch =
nativeEvent.touches[0].identifier;
}
} else if (isEndish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordEndTouchData);
nativeEvent.changedTouches.forEach(recordTouchEnd);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
for (var i = 0; i < touchBank.length; i++) {
var touchTrackToCheck = touchBank[i];
for (let i = 0; i < touchBank.length; i++) {
const touchTrackToCheck = touchBank[i];
if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
touchHistory.indexOfSingleActiveTouch = i;
break;
}
}
if (__DEV__) {
var activeTouchData = touchBank[touchHistory.indexOfSingleActiveTouch];
var foundActive = activeTouchData != null && !!activeTouchData.touchActive;
invariant(foundActive, 'Cannot find single active touch');
const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
warning(
activeRecord != null &&
activeRecord.touchActive,
'Cannot find single active touch.'
);
}
}
}
},
touchHistory: touchHistory,
touchHistory,
};
@@ -13,7 +13,6 @@
var ReactReconciler = require('ReactReconciler');
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
var instantiateReactComponent = require('instantiateReactComponent');
var KeyEscapeUtils = require('KeyEscapeUtils');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
@@ -24,6 +23,7 @@ function instantiateChild(childInstances, child, name, selfDebugID) {
// We found a component instance.
var keyUnique = (childInstances[name] === undefined);
if (__DEV__) {
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
warning(
keyUnique,
'flattenChildren(...): Encountered two children with the same key, ' +
@@ -34,7 +34,7 @@ function instantiateChild(childInstances, child, name, selfDebugID) {
);
}
if (child != null && keyUnique) {
childInstances[name] = instantiateReactComponent(child);
childInstances[name] = instantiateReactComponent(child, true);
}
}
@@ -125,7 +125,7 @@ var ReactChildReconciler = {
ReactReconciler.unmountComponent(prevChild, false);
}
// The child must be instantiated before it's mounted.
var nextChildInstance = instantiateReactComponent(nextElement);
var nextChildInstance = instantiateReactComponent(nextElement, true);
nextChildren[name] = nextChildInstance;
}
}
@@ -20,7 +20,6 @@ var ReactInstrumentation = require('ReactInstrumentation');
var ReactNodeTypes = require('ReactNodeTypes');
var ReactPropTypeLocations = require('ReactPropTypeLocations');
var ReactReconciler = require('ReactReconciler');
var ReactUpdateQueue = require('ReactUpdateQueue');
var checkReactTypeSpec = require('checkReactTypeSpec');
@@ -197,8 +196,10 @@ var ReactCompositeComponentMixin = {
var Component = this._currentElement.type;
var updateQueue = transaction.getUpdateQueue();
// Initialize the public class
var inst = this._constructComponent(publicProps, publicContext);
var inst = this._constructComponent(publicProps, publicContext, updateQueue);
var renderedElement;
// Support functional components
@@ -245,7 +246,7 @@ var ReactCompositeComponentMixin = {
inst.props = publicProps;
inst.context = publicContext;
inst.refs = emptyObject;
inst.updater = ReactUpdateQueue;
inst.updater = updateQueue;
this._instance = inst;
@@ -345,20 +346,20 @@ var ReactCompositeComponentMixin = {
return markup;
},
_constructComponent: function(publicProps, publicContext) {
_constructComponent: function(publicProps, publicContext, updateQueue) {
if (__DEV__) {
ReactCurrentOwner.current = this;
try {
return this._constructComponentWithoutOwner(publicProps, publicContext);
return this._constructComponentWithoutOwner(publicProps, publicContext, updateQueue);
} finally {
ReactCurrentOwner.current = null;
}
} else {
return this._constructComponentWithoutOwner(publicProps, publicContext);
return this._constructComponentWithoutOwner(publicProps, publicContext, updateQueue);
}
},
_constructComponentWithoutOwner: function(publicProps, publicContext) {
_constructComponentWithoutOwner: function(publicProps, publicContext, updateQueue) {
var Component = this._currentElement.type;
var instanceOrElement;
if (shouldConstruct(Component)) {
@@ -370,7 +371,7 @@ var ReactCompositeComponentMixin = {
);
}
}
instanceOrElement = new Component(publicProps, publicContext, ReactUpdateQueue);
instanceOrElement = new Component(publicProps, publicContext, updateQueue);
if (__DEV__) {
if (this._debugID !== 0) {
ReactInstrumentation.debugTool.onEndLifeCycleTimer(
@@ -390,7 +391,7 @@ var ReactCompositeComponentMixin = {
);
}
}
instanceOrElement = Component(publicProps, publicContext, ReactUpdateQueue);
instanceOrElement = Component(publicProps, publicContext, updateQueue);
if (__DEV__) {
if (this._debugID !== 0) {
ReactInstrumentation.debugTool.onEndLifeCycleTimer(
@@ -415,6 +416,11 @@ var ReactCompositeComponentMixin = {
try {
markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
} catch (e) {
if (__DEV__) {
if (this._debugID !== 0) {
ReactInstrumentation.debugTool.onError();
}
}
// Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint
transaction.rollback(checkpoint);
this._instance.unstable_handleError(e);
@@ -465,9 +471,11 @@ var ReactCompositeComponentMixin = {
renderedElement = this._renderValidatedComponent();
}
this._renderedNodeType = ReactNodeTypes.getType(renderedElement);
var nodeType = ReactNodeTypes.getType(renderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
renderedElement
renderedElement,
nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
);
this._renderedComponent = child;
if (__DEV__) {
@@ -975,9 +983,11 @@ var ReactCompositeComponentMixin = {
var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
ReactReconciler.unmountComponent(prevComponentInstance, false);
this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
var nodeType = ReactNodeTypes.getType(nextRenderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
nextRenderedElement
nextRenderedElement,
nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
);
this._renderedComponent = child;
if (__DEV__) {
@@ -162,10 +162,15 @@ if (__DEV__) {
}
};
setChildrenForInstrumentation = function(children) {
ReactInstrumentation.debugTool.onSetChildren(
getDebugID(this),
children ? Object.keys(children).map(key => children[key]._debugID) : []
);
var debugID = getDebugID(this);
// TODO: React Native empty components are also multichild.
// This means they still get into this method but don't have _debugID.
if (debugID !== 0) {
ReactInstrumentation.debugTool.onSetChildren(
debugID,
children ? Object.keys(children).map(key => children[key]._debugID) : []
);
}
};
}
@@ -7,10 +7,13 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNodeTypes
* @flow
*/
'use strict';
type ReactNodeType = 0 | 1 | 2;
var ReactElement = require('ReactElement');
var invariant = require('invariant');
@@ -20,7 +23,7 @@ var ReactNodeTypes = {
COMPOSITE: 1,
EMPTY: 2,
getType: function(node) {
getType: function(node: ReactElement<any>): ReactNodeType {
if (node === null || node === false) {
return ReactNodeTypes.EMPTY;
} else if (ReactElement.isValidElement(node)) {
@@ -246,8 +246,10 @@ var ReactUpdateQueue = {
enqueueUpdate(internalInstance);
},
enqueueElementInternal: function(internalInstance, newElement) {
internalInstance._pendingElement = newElement;
enqueueElementInternal: function(internalInstance, nextElement, nextContext) {
internalInstance._pendingElement = nextElement;
// TODO: introduce _pendingContext instead of setting it directly.
internalInstance._context = nextContext;
enqueueUpdate(internalInstance);
},
@@ -14,7 +14,6 @@
var CallbackQueue = require('CallbackQueue');
var PooledClass = require('PooledClass');
var ReactFeatureFlags = require('ReactFeatureFlags');
var ReactInstrumentation = require('ReactInstrumentation');
var ReactReconciler = require('ReactReconciler');
var Transaction = require('Transaction');
@@ -193,10 +192,6 @@ function runBatchedUpdates(transaction) {
}
var flushBatchedUpdates = function() {
if (__DEV__) {
ReactInstrumentation.debugTool.onBeginFlush();
}
// ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
// array and perform any updates enqueued by mount-ready handlers (i.e.,
// componentDidUpdate) but we need to check here too in order to catch
@@ -216,10 +211,6 @@ var flushBatchedUpdates = function() {
CallbackQueue.release(queue);
}
}
if (__DEV__) {
ReactInstrumentation.debugTool.onEndFlush();
}
};
/**
@@ -232,7 +223,7 @@ function enqueueUpdate(component) {
// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setProps, setState, forceUpdate, etc.; creation and
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.)
if (!batchingStrategy.isBatchingUpdates) {
@@ -239,6 +239,12 @@ function testPropsSequence(sequence) {
describe('ReactMultiChildReconcile', function() {
beforeEach(function() {
jest.resetModuleRegistry();
React = require('React');
ReactDOM = require('ReactDOM');
ReactDOMComponentTree = require('ReactDOMComponentTree');
ReactInstanceMap = require('ReactInstanceMap');
mapObject = require('mapObject');
});
it('should reset internal state if removed then readded', function() {
@@ -116,6 +116,10 @@ var expectClickLogsLengthToBe = function(instance, length) {
describe('reactiverefs', function() {
beforeEach(function() {
jest.resetModuleRegistry();
React = require('React');
ReactTestUtils = require('ReactTestUtils');
reactComponentExpect = require('reactComponentExpect');
});
/**
@@ -159,6 +163,10 @@ describe('reactiverefs', function() {
describe('ref swapping', function() {
beforeEach(function() {
jest.resetModuleRegistry();
React = require('React');
ReactTestUtils = require('ReactTestUtils');
reactComponentExpect = require('reactComponentExpect');
});
var RefHopsAround = React.createClass({
@@ -241,4 +249,3 @@ describe('ref swapping', function() {
expect(!!instance.refs).toBe(true);
});
});
@@ -78,14 +78,14 @@ var nextDebugID = 1;
* Given a ReactNode, create an instance that will actually be mounted.
*
* @param {ReactNode} node
* @param {boolean} shouldHaveDebugID
* @return {object} A new instance of the element's constructor.
* @protected
*/
function instantiateReactComponent(node) {
function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
var isEmpty = node === null || node === false;
if (isEmpty) {
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
@@ -141,16 +141,17 @@ function instantiateReactComponent(node) {
instance._mountImage = null;
if (__DEV__) {
var debugID = isEmpty ? 0 : nextDebugID++;
instance._debugID = debugID;
if (debugID !== 0) {
if (shouldHaveDebugID) {
var debugID = nextDebugID++;
instance._debugID = debugID;
var displayName = getDisplayName(instance);
ReactInstrumentation.debugTool.onSetDisplayName(debugID, displayName);
var owner = node && node._owner;
if (owner) {
ReactInstrumentation.debugTool.onSetOwner(debugID, owner._debugID);
}
} else {
instance._debugID = 0;
}
}
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactFeatureFlags
* @flow
*/
'use strict';
File renamed without changes.
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule accumulate
* @flow
*/
'use strict';
@@ -20,28 +21,27 @@ var invariant = require('invariant');
*
* @return {*|array<*>} An accumulation of items.
*/
function accumulate(current, next) {
function accumulate<T>(current: ?(T | Array<T>), next: T | Array<T>): T | Array<T> {
invariant(
next != null,
'accumulate(...): Accumulated items must be not be null or undefined.'
);
if (current == null) {
return next;
} else {
// Both are not empty. Warning: Never call x.concat(y) when you are not
// certain that x is an Array (x could be a string with concat method).
var currentIsArray = Array.isArray(current);
var nextIsArray = Array.isArray(next);
if (currentIsArray) {
return current.concat(next);
} else {
if (nextIsArray) {
return [current].concat(next);
} else {
return [current, next];
}
}
}
// Both are not empty. Warning: Never call x.concat(y) when you are not
// certain that x is an Array (x could be a string with concat method).
if (Array.isArray(current)) {
return current.concat(next);
}
if (Array.isArray(next)) {
return [current].concat(next);
}
return [current, next];
}
module.exports = accumulate;
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule accumulateInto
* @flow
*/
'use strict';
@@ -14,7 +15,6 @@
var invariant = require('invariant');
/**
*
* Accumulates items that must not be null or undefined into the first one. This
* is used to conserve memory by avoiding array allocations, and thus sacrifices
* API cleanness. Since `current` can be null before being passed in and not
@@ -27,31 +27,28 @@ var invariant = require('invariant');
* @return {*|array<*>} An accumulation of items.
*/
function accumulateInto(current, next) {
function accumulateInto<T>(current: ?(T | Array<T>), next: T | Array<T>): T | Array<T> {
invariant(
next != null,
'accumulateInto(...): Accumulated items must not be null or undefined.'
);
if (current == null) {
return next;
}
// Both are not empty. Warning: Never call x.concat(y) when you are not
// certain that x is an Array (x could be a string with concat method).
var currentIsArray = Array.isArray(current);
var nextIsArray = Array.isArray(next);
if (currentIsArray && nextIsArray) {
current.push.apply(current, next);
return current;
}
if (currentIsArray) {
if (Array.isArray(current)) {
if (Array.isArray(next)) {
current.push.apply(current, next);
return current;
}
current.push(next);
return current;
}
if (nextIsArray) {
if (Array.isArray(next)) {
// A bit too dangerous to mutate `next`.
return [current].concat(next);
}
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule adler32
* @flow
*/
'use strict';
@@ -18,7 +19,7 @@ var MOD = 65521;
// This implementation (a modified version of the SheetJS version) has been optimized
// for our use case, at the expense of conforming to the adler32 specification
// for non-ascii inputs.
function adler32(data) {
function adler32(data: string): number {
var a = 1;
var b = 0;
var i = 0;
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule forEachAccumulated
* @flow
*/
'use strict';
@@ -18,12 +19,16 @@
* handling the case when there is exactly one item (and we do not need to
* allocate an array).
*/
var forEachAccumulated = function(arr, cb, scope) {
function forEachAccumulated<T>(
arr: ?(T | Array<T>),
cb: ((elem: T) => void),
scope: ?any,
) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
};
}
module.exports = forEachAccumulated;
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule isTextInputElement
* @flow
*/
'use strict';
@@ -14,7 +15,7 @@
/**
* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
*/
var supportedInputTypes = {
var supportedInputTypes: {[key: string]: true | void} = {
'color': true,
'date': true,
'datetime': true,
@@ -32,12 +33,18 @@ var supportedInputTypes = {
'week': true,
};
function isTextInputElement(elem) {
function isTextInputElement(elem: ?HTMLElement): bool {
var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
return nodeName && (
(nodeName === 'input' && supportedInputTypes[elem.type]) ||
nodeName === 'textarea'
);
if (nodeName === 'input') {
return !!supportedInputTypes[((elem: any): HTMLInputElement).type];
}
if (nodeName === 'textarea') {
return true;
}
return false;
}
module.exports = isTextInputElement;
+1 -12
View File
@@ -120,17 +120,7 @@ var ReactHostMount = {
// }
// }
var instance = instantiateReactComponent(nextWrappedElement);
if (__DEV__) {
// Mute future events from the top level wrapper.
// It is an implementation detail that devtools should not know about.
instance._debugID = 0;
if (__DEV__) {
ReactInstrumentation.debugTool.onBeginFlush();
}
}
var instance = instantiateReactComponent(nextWrappedElement, false);
// The initial render is synchronous but any updates that happen during
// rendering, in componentWillMount or componentDidMount, will be batched
@@ -145,7 +135,6 @@ var ReactHostMount = {
ReactInstrumentation.debugTool.onMountRootComponent(
instance._renderedComponent._debugID
);
ReactInstrumentation.debugTool.onEndFlush();
}
return new ReactTestInstance(instance);
},
@@ -14,6 +14,7 @@
var CallbackQueue = require('CallbackQueue');
var PooledClass = require('PooledClass');
var Transaction = require('Transaction');
var ReactUpdateQueue = require('ReactUpdateQueue');
/**
* Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during
@@ -81,6 +82,13 @@ var Mixin = {
return this.reactMountReady;
},
/**
* @return {object} The queue to collect React async events.
*/
getUpdateQueue: function() {
return ReactUpdateQueue;
},
/**
* `PooledClass` looks for this, and will invoke this before allowing this
* instance to be reused.
+4 -3
View File
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule KeyEscapeUtils
* @flow
*/
'use strict';
@@ -14,10 +15,10 @@
/**
* Escape and wrap key so it is safe to use as a reactid
*
* @param {*} key to be escaped.
* @param {string} key to be escaped.
* @return {string} the escaped key.
*/
function escape(key) {
function escape(key: string): string {
var escapeRegex = /[=:]/g;
var escaperLookup = {
'=': '=0',
@@ -39,7 +40,7 @@ function escape(key) {
* @param {string} key to unescape.
* @return {string} the unescaped key.
*/
function unescape(key) {
function unescape(key: string): string {
var unescapeRegex = /(=0|=2)/g;
var unescaperLookup = {
'=0': '=',
+1 -1
View File
@@ -94,7 +94,7 @@ var DEFAULT_POOLER = oneArgumentPooler;
* Augments `CopyConstructor` to be a poolable class, augmenting only the class
* itself (statically) not adding any prototypical fields. Any CopyConstructor
* you give this may have a `poolSize` property, and will look for a
* prototypical `destructor` on instances (optional).
* prototypical `destructor` on instances.
*
* @param {Function} CopyConstructor Constructor that can be used to reset.
* @param {Function} pooler Customizable pooler.
+14 -2
View File
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule deprecated
* @flow
*/
'use strict';
@@ -24,7 +25,13 @@ var warning = require('warning');
* @param {function} fn The function to forward on to
* @return {function} The function that will warn once and then call fn
*/
function deprecated(fnName, newModule, newPackage, ctx, fn) {
function deprecated<T: Function>(
fnName: string,
newModule: string,
newPackage: string,
ctx: mixed,
fn: T,
): T {
var warned = false;
if (__DEV__) {
var newFn = function() {
@@ -47,7 +54,12 @@ function deprecated(fnName, newModule, newPackage, ctx, fn) {
};
// We need to make sure all properties of the original fn are copied over.
// In particular, this is needed to support PropTypes
return Object.assign(newFn, fn);
Object.assign(newFn, (fn: Object));
// Flow is not smart enough to figure out that newFn is of the same type as
// fn. Since we don't want to lose out the type of the function, casting
// to any and force flow to use T.
return ((newFn: any): T);
}
return fn;
+28 -17
View File
@@ -7,11 +7,11 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule flattenChildren
* @flow
*/
'use strict';
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
var KeyEscapeUtils = require('KeyEscapeUtils');
var traverseAllChildren = require('traverseAllChildren');
var warning = require('warning');
@@ -22,22 +22,30 @@ var warning = require('warning');
* @param {!string} name String name of key path to child.
* @param {number=} selfDebugID Optional debugID of the current internal instance.
*/
function flattenSingleChildIntoContext(traverseContext, child, name, selfDebugID) {
function flattenSingleChildIntoContext(
traverseContext: mixed,
child: ReactElement<any>,
name: string,
selfDebugID?: number,
): void {
// We found a component instance.
var result = traverseContext;
var keyUnique = (result[name] === undefined);
if (__DEV__) {
warning(
keyUnique,
'flattenChildren(...): Encountered two children with the same key, ' +
'`%s`. Child keys must be unique; when two children share a key, only ' +
'the first child will be used.%s',
KeyEscapeUtils.unescape(name),
ReactComponentTreeDevtool.getStackAddendumByID(selfDebugID)
);
}
if (keyUnique && child != null) {
result[name] = child;
if (traverseContext && typeof traverseContext === 'object') {
const result = traverseContext;
const keyUnique = (result[name] === undefined);
if (__DEV__) {
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
warning(
keyUnique,
'flattenChildren(...): Encountered two children with the same key, ' +
'`%s`. Child keys must be unique; when two children share a key, only ' +
'the first child will be used.%s',
KeyEscapeUtils.unescape(name),
ReactComponentTreeDevtool.getStackAddendumByID(selfDebugID)
);
}
if (keyUnique && child != null) {
result[name] = child;
}
}
}
@@ -46,7 +54,10 @@ function flattenSingleChildIntoContext(traverseContext, child, name, selfDebugID
* children will not be included in the resulting object.
* @return {!object} flattened children keyed by name.
*/
function flattenChildren(children, selfDebugID) {
function flattenChildren(
children: ReactElement<any>,
selfDebugID?: number,
): ?{ [name: string]: ReactElement<any> } {
if (children == null) {
return children;
}
+2 -1
View File
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule getIteratorFn
* @flow
*/
'use strict';
@@ -29,7 +30,7 @@ var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
* @param {?object} maybeIterable
* @return {?function}
*/
function getIteratorFn(maybeIterable) {
function getIteratorFn(maybeIterable: ?any): ?(p: ReactElement<any>) => void {
var iteratorFn = maybeIterable && (
(ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL]) ||
maybeIterable[FAUX_ITERATOR_SYMBOL]
+3 -2
View File
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule reactProdInvariant
* @flow
*/
'use strict';
@@ -16,7 +17,7 @@
* and will _only_ be required by the corresponding babel pass.
* It always throws.
*/
function reactProdInvariant(code) {
function reactProdInvariant(code: string): void {
var argCount = arguments.length - 1;
var message = (
@@ -33,7 +34,7 @@ function reactProdInvariant(code) {
' for full errors and additional helpful warnings.'
);
var error = new Error(message);
var error: Error & { framesToPop?: number } = new Error(message);
error.name = 'Invariant Violation';
error.framesToPop = 1; // we don't care about reactProdInvariant's own frame