diff --git a/.flowconfig b/.flowconfig index 94ffcb54a2ef..d15db1c9ab7e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -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 diff --git a/examples/fiber/index.html b/examples/fiber/index.html new file mode 100644 index 000000000000..c50799379495 --- /dev/null +++ b/examples/fiber/index.html @@ -0,0 +1,44 @@ + + + + + Fiber Example + + + +

Fiber Example

+
+

+ To install React, follow the instructions on + GitHub. +

+

+ If you can see this, React is not working right. + If you checked out the source from GitHub make sure to run grunt. +

+
+ + + + + diff --git a/package.json b/package.json index afa8ed8947ee..67ac99e5f847 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js new file mode 100644 index 000000000000..74369ac3ea7a --- /dev/null +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -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) { + 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) : void { + container.innerHTML = ''; + recursivelyAppendChildren(container, children); + }, + + createInstance(type : string, props : Props, children : HostChildren) : 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) : boolean { + return true; + }, + + commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren) : 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, container : DOMContainerElement) { + if (!container._reactRootContainer) { + container._reactRootContainer = DOMRenderer.mountContainer(element, container); + } else { + DOMRenderer.updateContainer(element, container._reactRootContainer); + } + }, + +}; + +module.exports = ReactDOM; diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 9214a1865b9f..5682dc9f8a70 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -20,33 +20,97 @@ '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 }; +type Props = { }; +type Instance = { tag: 99, type: string, id: number, children: Array }; + +var instanceCounter = 0; + +function recursivelyAppendChildren(flatArray : Array, child : HostChildren) { + 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) { + const flatArray = []; + recursivelyAppendChildren(flatArray, children); + return flatArray; +} + var NoopRenderer = ReactFiberReconciler({ - createHostInstance() { + updateContainer(containerInfo : Container, children : HostChildren) : void { + console.log('Update container #' + containerInfo.rootID); + containerInfo.children = flattenChildren(children); + }, + createInstance(type : string, props : Props, children : HostChildren) : 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) : boolean { + console.log('Prepare for update on #' + instance.id); + return true; + }, + + commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren) : 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) { if (!root) { - root = NoopRenderer.mountContainer(element, null); + root = NoopRenderer.mountContainer(element, rootContainer); } else { NoopRenderer.updateContainer(element, root); } @@ -91,6 +155,19 @@ var ReactNoop = { console.log('Nothing rendered yet.'); return; } + + function logHostInstances(children: Array, 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) { @@ -100,6 +177,10 @@ var ReactNoop = { logFiber(fiber.sibling, depth); } } + + console.log('HOST INSTANCES:'); + logContainer(rootContainer, 0); + console.log('FIBERS:'); logFiber((root.stateNode : any).current, 0); }, diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 48363ac2a053..39683f001e55 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -48,7 +48,7 @@ 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 @@ -82,6 +82,16 @@ export type Fiber = Instance & { // 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. pendingWorkPriority: PriorityLevel, @@ -129,6 +139,10 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { memoizedProps: null, output: null, + nextEffect: null, + firstEffect: null, + lastEffect: null, + pendingWorkPriority: NoWork, hasWorkInProgress: false, @@ -157,6 +171,13 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi 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; } diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index ec1d78af0391..4fcf4d87b081 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -14,6 +14,7 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; +import type { HostConfig } from 'ReactFiberReconciler'; var ReactChildFiber = require('ReactChildFiber'); var ReactTypeOfWork = require('ReactTypeOfWork'); @@ -33,211 +34,217 @@ var { } = require('ReactPriorityLevel'); var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork'); -function reconcileChildren(current, workInProgress, nextChildren) { - const priority = workInProgress.pendingWorkPriority; - workInProgress.child = ReactChildFiber.reconcileChildFibers( - workInProgress, - current ? current.child : null, - nextChildren, - priority - ); -} - -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; -} - -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; - } else { +module.exports = function(config : HostConfig) { + + function reconcileChildren(current, workInProgress, nextChildren) { + const priority = workInProgress.pendingWorkPriority; workInProgress.child = ReactChildFiber.reconcileChildFibers( workInProgress, current ? current.child : null, nextChildren, priority ); + } + + 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; - return workInProgress.child; } -} - -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; + + 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; + } else { + workInProgress.child = ReactChildFiber.reconcileChildFibers( + workInProgress, + current ? current.child : null, + nextChildren, + priority + ); + workInProgress.pendingWorkPriority = NoWork; + return workInProgress.child; } } - 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; + + 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 { - workInProgress.child = null; - return null; + 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; } - 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; + 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; } - 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 - ); + 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; } - return workInProgress.child; - case YieldComponent: - // A yield component is just a placeholder, we can just run through the - // next one immediately. + } 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; - if (workInProgress.sibling) { - return beginWork( - workInProgress.sibling.alternate, - workInProgress.sibling - ); + 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; - default: - throw new Error('Unknown unit of work tag'); + } + + 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'); + } } -} -exports.beginWork = beginWork; + return { + beginWork, + }; + +}; diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js new file mode 100644 index 000000000000..7792be2102bb --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -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(config : HostConfig) { + + 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, + }; + +}; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index d816510b4001..9900a5b9fb9d 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -14,7 +14,7 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; - +import type { HostConfig } from 'ReactFiberReconciler'; import type { ReifiedYield } from 'ReactReifiedYield'; var ReactChildFiber = require('ReactChildFiber'); @@ -30,102 +30,162 @@ var { YieldComponent, } = ReactTypeOfWork; -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; -} - -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); +module.exports = function(config : HostConfig) { + + const createInstance = config.createInstance; + const prepareUpdate = config.prepareUpdate; + + 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; + } + } + + /* + 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; + } + */ + + 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; } -} -function moveCoroutineToHandlerPhase(current : ?Fiber, workInProgress : Fiber) { - var coroutine = (workInProgress.pendingProps : ?ReactCoroutine); - if (!coroutine) { - throw new Error('Should be resolved by now'); + 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); + } } - // 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 = []; - var child = workInProgress.child; - while (child) { - recursivelyFillYields(yields, child.output); - child = child.sibling; + 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 = []; + 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; } - 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; -} - -exports.completeWork = function(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: - return null; - case HostComponent: - transferOutput(workInProgress.child, workInProgress); - console.log('/host component', workInProgress.type); - 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'); + + 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, + }; + }; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 3fde615cf0ec..cb22e88421dc 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -14,6 +14,7 @@ import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; +import type { TypeOfWork } from 'ReactTypeOfWork'; var { createFiberRoot } = require('ReactFiberRoot'); var ReactFiberScheduler = require('ReactFiberScheduler'); @@ -22,18 +23,27 @@ var { LowPriority, } = require('ReactPriorityLevel'); -type ReactHostElement = { - type: T, - props: P -}; - type Deadline = { timeRemaining : () => number }; -export type HostConfig = { +type HostChildNode = { tag: TypeOfWork, output: HostChildren, sibling: any }; + +export type HostChildren = null | void | I | HostChildNode; + +export type HostConfig = { + + // 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) : void; + + createInstance(type : T, props : P, children : HostChildren) : I, + prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren) : bool, + commitUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren) : void, + deleteInstance(instance : I) : void, - createHostInstance(element : ReactHostElement) : I, scheduleHighPriCallback(callback : () => void) : void, scheduleLowPriCallback(callback : (deadline : Deadline) => void) : void @@ -41,22 +51,22 @@ export type HostConfig = { type OpaqueNode = Fiber; -export type Reconciler = { - mountContainer(element : ReactElement, containerInfo : ?Object) : OpaqueNode, +export type Reconciler = { + mountContainer(element : ReactElement, containerInfo : C) : OpaqueNode, updateContainer(element : ReactElement, container : OpaqueNode) : void, unmountContainer(container : OpaqueNode) : void, // Used to extract the return value from the initial render. Legacy API. - getPublicRootInstance(container : OpaqueNode) : ?Object, + getPublicRootInstance(container : OpaqueNode) : (C | null), }; -module.exports = function(config : HostConfig) : Reconciler { +module.exports = function(config : HostConfig) : Reconciler { var { scheduleLowPriWork } = ReactFiberScheduler(config); return { - mountContainer(element : ReactElement, containerInfo : ?Object) : OpaqueNode { + mountContainer(element : ReactElement, containerInfo : C) : OpaqueNode { const root = createFiberRoot(containerInfo); const container = root.current; // TODO: Use pending work/state instead of props. @@ -91,7 +101,7 @@ module.exports = function(config : HostConfig) : Reconciler { scheduleLowPriWork(root); }, - getPublicRootInstance(container : OpaqueNode) : ?Object { + getPublicRootInstance(container : OpaqueNode) : (C | null) { return null; }, diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index ce186c3fced2..c0b312253cc4 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -18,7 +18,7 @@ const { createHostContainerFiber } = require('ReactFiber'); export type FiberRoot = { // Any additional information from the host associated with this root. - containerInfo: ?Object, + 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. @@ -27,7 +27,7 @@ export type FiberRoot = { nextScheduledRoot: ?FiberRoot, }; -exports.createFiberRoot = function(containerInfo : ?Object) : FiberRoot { +exports.createFiberRoot = function(containerInfo : any) : FiberRoot { // Cyclic construction. This cheats the type system right now because // stateNode is any. const uninitializedFiber = createHostContainerFiber(); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4b6da086bf92..6df6198f9fe6 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -16,9 +16,11 @@ 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 { beginWork } = require('ReactFiberBeginWork'); -var { completeWork } = require('ReactFiberCompleteWork'); var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork'); var { @@ -30,7 +32,11 @@ var { var timeHeuristicForUnitOfWork = 1; -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { + + const { beginWork } = ReactFiberBeginWork(config); + const { completeWork } = ReactFiberCompleteWork(config); + const { commitWork } = ReactFiberCommitWork(config); // const scheduleHighPriCallback = config.scheduleHighPriCallback; const scheduleLowPriCallback = config.scheduleLowPriCallback; @@ -78,6 +84,22 @@ module.exports = function(config : HostConfig) { 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. @@ -96,11 +118,25 @@ module.exports = function(config : HostConfig) { const returnFiber = workInProgress.return; - // Ensure that remaining work priority bubbles up. - if (returnFiber && workInProgress.pendingWorkPriority !== NoWork && - (returnFiber.pendingWorkPriority === NoWork || - returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) { - returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority; + 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) { @@ -121,6 +157,7 @@ module.exports = function(config : HostConfig) { // 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 diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js new file mode 100644 index 000000000000..c59c12b97d31 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -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 {props.text}; + } + + function Foo(props) { + return ( +
+ + {props.text === 'World' ? : null} +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(span()), + ]); + + ReactNoop.render(); + 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 {props.text}; + } + + function Foo(props) { + return ( +
+
+ + {props.text === 'Hello' ? : null} +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(div(span(), span()), span()), + ]); + + ReactNoop.render(); + 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? + +});