Skip to content

Commit

Permalink
Restore DOM selection and suppress events
Browse files Browse the repository at this point in the history
This makes Draft mostly work.
  • Loading branch information
sophiebits committed Nov 23, 2016
1 parent c7129ce commit 7838575
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 78 deletions.
3 changes: 0 additions & 3 deletions scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ src/renderers/dom/shared/eventPlugins/__tests__/SimpleEventPlugin-test.js
* should not forward clicks when it becomes disabled
* should work correctly if the listener is changed

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should control a value in reentrant events

src/renderers/dom/stack/client/__tests__/ReactDOM-test.js
* throws in render() if the mount callback is not a function
* throws in render() if the update callback is not a function
Expand Down
2 changes: 2 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMIframe-test.js

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should properly control a value even if no event listener exists
* should control a value in reentrant events
* should control values in reentrant events with different targets
* should display `defaultValue` of number 0
* only assigns defaultValue if it changes
Expand Down Expand Up @@ -1023,6 +1024,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
* can schedule updates after uncaught error during umounting
* continues work on other roots despite caught errors
* continues work on other roots despite uncaught errors
* restores environment state despite error during commit

src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
* handles isMounted even when the initial render is deferred
Expand Down
21 changes: 16 additions & 5 deletions src/renderers/dom/fiber/ReactDOMFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,21 @@ import type { Fiber } from 'ReactFiber';
import type { HostChildren } from 'ReactFiberReconciler';
import type { ReactNodeList } from 'ReactTypes';

var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactControlledComponent = require('ReactControlledComponent');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var ReactDOMFiberComponent = require('ReactDOMFiberComponent');
var ReactDOMInjection = require('ReactDOMInjection');
var ReactFiberReconciler = require('ReactFiberReconciler');
var ReactInputSelection = require('ReactInputSelection');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactPortal = require('ReactPortal');

var findDOMNode = require('findDOMNode');
var invariant = require('invariant');
var warning = require('warning');

ReactDOMInjection.inject();
ReactControlledComponent.injection.injectFiberControlledHostComponent(
ReactDOMFiberComponent
);

var {
createElement,
setInitialProperties,
Expand Down Expand Up @@ -73,8 +70,22 @@ function recursivelyAppendChildren(parent : Element, child : HostChildren<Instan
}
}

let eventsEnabled : ?boolean = false;
let selectionInformation : ?mixed = null;

var DOMRenderer = ReactFiberReconciler({

prepareForCommit() : void {
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
ReactBrowserEventEmitter.setEnabled(false);
selectionInformation = ReactInputSelection.getSelectionInformation();
},

resetAfterCommit() : void {
ReactInputSelection.restoreSelection(selectionInformation);
ReactBrowserEventEmitter.setEnabled(eventsEnabled);
},

updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
// TODO: Containers should update similarly to other parents.
container.innerHTML = '';
Expand Down
17 changes: 17 additions & 0 deletions src/renderers/noop/ReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,23 @@ var NoopRenderer = ReactFiberReconciler({
scheduledDeferredCallback = callback;
},

prepareForCommit() : void {
if (isCommitting) {
throw new Error('Double prepare before commit');
}
isCommitting = true;
},

resetAfterCommit() : void {
if (!isCommitting) {
throw new Error('Double reset after commit');
}
isCommitting = false;
},

});

var isCommitting = false;
var rootContainers = new Map();
var roots = new Map();
var DEFAULT_ROOT_ID = '<default>';
Expand Down Expand Up @@ -255,6 +270,8 @@ var ReactNoop = {

syncUpdates: NoopRenderer.syncUpdates,

isCommitting: () => isCommitting,

// Logs the current state of the tree.
dumpTree(rootID : string = DEFAULT_ROOT_ID) {
const root = roots.get(rootID);
Expand Down
3 changes: 3 additions & 0 deletions src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export type HostConfig<T, P, I, TI, C> = {
scheduleAnimationCallback(callback : () => void) : void,
scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void,

prepareForCommit() : void,
resetAfterCommit() : void,

useSyncScheduling ?: boolean,
};

Expand Down
156 changes: 86 additions & 70 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
// Need this to prevent recursion while in a Task loop.
let isPerformingTaskWork : boolean = false;

// We'll only prepare/reset on the outermost commit even when a setState
// callback causes another synchronous rerender
let isCommitting : boolean = false;

// The next work in progress fiber that we're currently working on.
let nextUnitOfWork : ?Fiber = null;
let nextPriorityLevel : PriorityLevel = NoWork;
Expand Down Expand Up @@ -161,83 +165,95 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}

function commitAllWork(finishedWork : Fiber) {
// Commit all the side-effects within a tree.
// First, we'll perform all the host insertions, updates, deletions and
// ref unmounts.
let effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
switch (effectfulFiber.effectTag) {
case Placement:
case PlacementAndCallback: {
commitInsertion(effectfulFiber);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
effectfulFiber.effectTag ^= Placement;
break;
if (!isCommitting) {
isCommitting = true;
config.prepareForCommit();
}

try {
// Commit all the side-effects within a tree.
// First, we'll perform all the host insertions, updates, deletions and
// ref unmounts.
let effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
switch (effectfulFiber.effectTag) {
case Placement:
case PlacementAndCallback: {
commitInsertion(effectfulFiber);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
effectfulFiber.effectTag ^= Placement;
break;
}
case PlacementAndUpdate:
case PlacementAndUpdateAndCallback: {
// Placement
commitInsertion(effectfulFiber);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
effectfulFiber.effectTag ^= Placement;

// Update
const current = effectfulFiber.alternate;
commitWork(current, effectfulFiber);
break;
}
case Update:
case UpdateAndCallback:
const current = effectfulFiber.alternate;
commitWork(current, effectfulFiber);
break;
case Deletion:
case DeletionAndCallback:
commitDeletion(effectfulFiber);
break;
}
case PlacementAndUpdate:
case PlacementAndUpdateAndCallback: {
// Placement
commitInsertion(effectfulFiber);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
effectfulFiber.effectTag ^= Placement;

// Update

effectfulFiber = effectfulFiber.nextEffect;
}

// Next, we'll perform all life-cycles and ref callbacks. Life-cycles
// happens as a separate pass so that all effects in the entire tree have
// already been invoked.
effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
if (effectfulFiber.effectTag & (Update | Callback)) {
const current = effectfulFiber.alternate;
commitWork(current, effectfulFiber);
break;
const previousPriorityContext = priorityContext;
priorityContext = TaskPriority;
try {
commitLifeCycles(current, effectfulFiber);
} finally {
priorityContext = previousPriorityContext;
}
}
case Update:
case UpdateAndCallback:
const current = effectfulFiber.alternate;
commitWork(current, effectfulFiber);
break;
case Deletion:
case DeletionAndCallback:
commitDeletion(effectfulFiber);
break;
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;
// Ensure that we reset the effectTag here so that we can rely on effect
// tags to reason about the current life-cycle.
effectfulFiber = next;
}

effectfulFiber = effectfulFiber.nextEffect;
}
// Finally if the root itself had an effect, we perform that since it is not
// part of the effect list.
if (finishedWork.effectTag !== NoEffect) {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
commitLifeCycles(current, finishedWork);
}

// Next, we'll perform all life-cycles and ref callbacks. Life-cycles
// happens as a separate pass so that all effects in the entire tree have
// already been invoked.
effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
if (effectfulFiber.effectTag & (Update | Callback)) {
const current = effectfulFiber.alternate;
const previousPriorityContext = priorityContext;
priorityContext = TaskPriority;
try {
commitLifeCycles(current, effectfulFiber);
} finally {
priorityContext = previousPriorityContext;
}
// The task work includes batched updates and error handling.
performTaskWork();
} finally {
if (isCommitting) {
isCommitting = false;
config.resetAfterCommit();
}
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;
// Ensure that we reset the effectTag here so that we can rely on effect
// tags to reason about the current life-cycle.
effectfulFiber = next;
}

// Finally if the root itself had an effect, we perform that since it is not
// part of the effect list.
if (finishedWork.effectTag !== NoEffect) {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
commitLifeCycles(current, finishedWork);
}

// The task work includes batched updates and error handling.
performTaskWork();
}
}

function resetWorkPriority(workInProgress : Fiber) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,24 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren('e')).toEqual(null);
expect(ReactNoop.getChildren('f')).toEqual(null);
});

it('restores environment state despite error during commit', () => {
let ranDidMount = false;
class Broken extends React.Component {
render() {
expect(ReactNoop.isCommitting()).toBe(false);
return <div />;
}
componentDidMount() {
expect(ReactNoop.isCommitting()).toBe(true);
ranDidMount = true;
throw new Error('Hello');
}
}

ReactNoop.render(<Broken />);
expect(() => ReactNoop.flush()).toThrow('Hello');
expect(ReactNoop.isCommitting()).toBe(false);
expect(ranDidMount).toBe(true);
});
});

0 comments on commit 7838575

Please sign in to comment.