From c87ffc0bebaabe69dfbd7b385480da614c5dc0da Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 8 Dec 2016 13:10:47 -0800 Subject: [PATCH] [Fiber] Support SVG (#8490) * Test that SVG elements get created with the right namespace * Pass root to the renderer methods * Keep track of host instances and containers * Keep instances instead of fibers on the stack * Create text instances in begin phase * Create instance before bailing on offscreen children Otherwise, the parent gets skipped next time. We could probably create it later but this seems simpler. * Tweak magic numbers in incremental tests I don't understand why they changed but probably related to us moving some work into begin phase? * Only push newly created nodes on the parent stack Previously I was pushing nodes on the parent stack regardless of whether they were already in current or not. As a result, insertions during updates were duplicated, and nodes were added to existing parents before commit phase. Luckily we have a test that caught that. * Fix lint * Fix Flow I had to wrap HostContext API into a closure so that it's parameterizeable with I and C. * Use the same destructuring style in scheduler as everywhere else * Remove branches that don't seem to run anymore I'm not 100% sure this is right but I can't get tests to fail. * Be explicit about the difference between type and tag I was confused by th HACK comment so I learned how DOM and SVG work with casing and tried to write a more descriptive comment. It also seems like passing fiber.type into finalizeInitialChildren() is a potential problem because DOM code assumes tag is lowercase. So I added a similar "hack" to finalizeInitialChildren() that is identical to the one we have prepareUpdate() so if we fix them later, we fix both. * Save and restore host context when pushing and popping portals * Revert parent context and adding children in the begin phase We can address this later separately as it is a more hot path. This doesn't affect correctness of SVG container behavior. * Add a test for SVG updates This tests the "jump" reuse code path in particular. * Record tests * Read ownerDocument from the root container instance This way createInstance() depends on the innermost container only for reading the namespace. * Track namespaces instead of creating instances early While we might want to create instance in the begin phase, we shouldn't let DOM guide reconciler design. Instead, we are adding a new concept of "host context". In case of ReactDOMFiber, it's just the current namespace. We are keeping a stack of host context values, ignoring those that are referentially equal. The renderer receives the parent context and type, and can return a new context. * Pop child context before reading own context and clarify API It wasn't quite clear from the API which context was being returned by the renderer. Changed the API to specifically ask for child context, and thus to pop it before getting the current context. This fixes the case with to which I intended to give SVG namespace. * Give SVG namespace to itself * Don't allocate unnecessarily when reconciling portals We create stacks lazily so that if portal doesn't contain s, we don't need to allocate. We also reuse the same object for portal host context state instead of creating a new one every time. * Add more tests for edge cases * Fix up math namespace * Maintain a separate container stack * Fix rebase mistakes * Unwind context on errors * Reset the container state when reusing the object * Add getChildHostContext() to ReactART * Record tests --- scripts/fiber/tests-passing.txt | 8 + src/renderers/art/ReactARTFiber.js | 4 + src/renderers/dom/fiber/ReactDOMFiber.js | 43 ++- .../dom/fiber/ReactDOMFiberComponent.js | 69 +++-- .../dom/fiber/__tests__/ReactDOMFiber-test.js | 292 ++++++++++++++++++ .../dom/shared/__tests__/ReactDOMSVG-test.js | 127 ++++++++ src/renderers/noop/ReactNoop.js | 6 +- .../shared/fiber/ReactFiberBeginWork.js | 43 ++- .../shared/fiber/ReactFiberCommitWork.js | 26 +- .../shared/fiber/ReactFiberCompleteWork.js | 39 ++- .../shared/fiber/ReactFiberHostContext.js | 157 ++++++++++ .../shared/fiber/ReactFiberReconciler.js | 14 +- .../shared/fiber/ReactFiberScheduler.js | 55 +++- 13 files changed, 797 insertions(+), 86 deletions(-) create mode 100644 src/renderers/shared/fiber/ReactFiberHostContext.js diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index f277e317d9..cb71a3431d 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -514,6 +514,12 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js * should render one portal * should render many portals * should render nested portals +* should keep track of namespace across portals (simple) +* should keep track of namespace across portals (medium) +* should keep track of namespace across portals (complex) +* should unwind namespaces on uncaught errors +* should unwind namespaces on caught errors +* should unwind namespaces on caught errors in a portal * should pass portal context when rendering subtree elsewhere * should update portal context if it changes due to setState * should update portal context if it changes due to re-render @@ -669,6 +675,8 @@ src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js * creates initial namespaced markup +* creates elements with SVG namespace inside SVG tag during mount +* creates elements with SVG namespace inside SVG tag during update src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js * updates a mounted text component in place diff --git a/src/renderers/art/ReactARTFiber.js b/src/renderers/art/ReactARTFiber.js index 467f976463..8655b8b8e2 100644 --- a/src/renderers/art/ReactARTFiber.js +++ b/src/renderers/art/ReactARTFiber.js @@ -485,6 +485,10 @@ const ARTRenderer = ReactFiberReconciler({ // Noop }, + getChildHostContext() { + return null; + }, + scheduleAnimationCallback: window.requestAnimationFrame, scheduleDeferredCallback: window.requestIdleCallback, diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index c4c851d8e3..5b579517c9 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -33,6 +33,7 @@ var warning = require('warning'); var { createElement, + getChildNamespace, setInitialProperties, updateProperties, } = ReactDOMFiberComponent; @@ -60,6 +61,11 @@ let selectionInformation : ?mixed = null; var DOMRenderer = ReactFiberReconciler({ + getChildHostContext(parentHostContext : string | null, type : string) { + const parentNamespace = parentHostContext; + return getChildNamespace(parentNamespace, type); + }, + prepareForCommit() : void { eventsEnabled = ReactBrowserEventEmitter.isEnabled(); ReactBrowserEventEmitter.setEnabled(false); @@ -76,11 +82,11 @@ var DOMRenderer = ReactFiberReconciler({ createInstance( type : string, props : Props, - internalInstanceHandle : Object + rootContainerInstance : Container, + hostContext : string | null, + internalInstanceHandle : Object, ) : Instance { - const root = document.documentElement; // HACK - - const domElement : Instance = createElement(type, props, root); + const domElement : Instance = createElement(type, props, rootContainerInstance, hostContext); precacheFiberNode(internalInstanceHandle, domElement); return domElement; }, @@ -89,10 +95,18 @@ var DOMRenderer = ReactFiberReconciler({ parentInstance.appendChild(child); }, - finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void { - const root = document.documentElement; // HACK - - setInitialProperties(domElement, type, props, root); + finalizeInitialChildren( + domElement : Instance, + props : Props, + rootContainerInstance : Container, + ) : void { + // TODO: we normalize here because DOM renderer expects tag to be lowercase. + // We can change DOM renderer to compare special case against upper case, + // and use tagName (which is upper case for HTML DOM elements). Or we could + // let the renderer "normalize" the fiber type so we don't have to read + // the type from DOM. However we need to remember SVG is case-sensitive. + var tag = domElement.tagName.toLowerCase(); + setInitialProperties(domElement, tag, props, rootContainerInstance); }, prepareUpdate( @@ -107,14 +121,19 @@ var DOMRenderer = ReactFiberReconciler({ domElement : Instance, oldProps : Props, newProps : Props, - internalInstanceHandle : Object + rootContainerInstance : Container, + internalInstanceHandle : Object, ) : void { - var type = domElement.tagName.toLowerCase(); // HACK - var root = document.documentElement; // HACK + // TODO: we normalize here because DOM renderer expects tag to be lowercase. + // We can change DOM renderer to compare special case against upper case, + // and use tagName (which is upper case for HTML DOM elements). Or we could + // let the renderer "normalize" the fiber type so we don't have to read + // the type from DOM. However we need to remember SVG is case-sensitive. + var tag = domElement.tagName.toLowerCase(); // Update the internal instance handle so that we know which props are // the current ones. precacheFiberNode(internalInstanceHandle, domElement); - updateProperties(domElement, type, oldProps, newProps, root); + updateProperties(domElement, tag, oldProps, newProps, rootContainerInstance); }, shouldSetTextContent(props : Props) : boolean { diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 7c6ebd9b3c..826dcc5b6a 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -46,6 +46,11 @@ var CHILDREN = 'children'; var STYLE = 'style'; var HTML = '__html'; +var { + svg: SVG_NAMESPACE, + mathml: MATH_NAMESPACE, +} = DOMNamespaces; + // Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE). var DOC_FRAGMENT_TYPE = 11; @@ -451,45 +456,49 @@ function updateDOMProperties( } } -var ReactDOMFiberComponent = { +// Assumes there is no parent namespace. +function getIntrinsicNamespace(type : string) : string | null { + switch (type) { + case 'svg': + return SVG_NAMESPACE; + case 'math': + return MATH_NAMESPACE; + default: + return null; + } +} - // TODO: Use this to keep track of changes to the host context and use this - // to determine whether we switch to svg and back. - // TODO: Does this need to check the current namespace? In case these tags - // happen to be valid in some other namespace. - isNewHostContainer(tag : string) { - return tag === 'svg' || tag === 'foreignobject'; +var ReactDOMFiberComponent = { + getChildNamespace(parentNamespace : string | null, type : string) : string | null { + if (parentNamespace == null) { + // No parent namespace: potential entry point. + return getIntrinsicNamespace(type); + } + if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') { + // We're leaving SVG. + return null; + } + // By default, pass namespace below. + return parentNamespace; }, createElement( - tag : string, + type : string, props : Object, - rootContainerElement : Element + rootContainerElement : Element, + parentNamespace : string | null ) : Element { - validateDangerousTag(tag); + validateDangerousTag(type); // TODO: - // tag.toLowerCase(); Do we need to apply lower case only on non-custom elements? + // const tag = type.toLowerCase(); Do we need to apply lower case only on non-custom elements? // We create tags in the namespace of their parent container, except HTML // tags get no namespace. - var namespaceURI = rootContainerElement.namespaceURI; - if (namespaceURI == null || - namespaceURI === DOMNamespaces.svg && - rootContainerElement.tagName === 'foreignObject') { - namespaceURI = DOMNamespaces.html; - } - if (namespaceURI === DOMNamespaces.html) { - if (tag === 'svg') { - namespaceURI = DOMNamespaces.svg; - } else if (tag === 'math') { - namespaceURI = DOMNamespaces.mathml; - } - // TODO: Make this a new root container element. - } - var ownerDocument = rootContainerElement.ownerDocument; var domElement : Element; - if (namespaceURI === DOMNamespaces.html) { + var namespaceURI = parentNamespace || getIntrinsicNamespace(type); + if (namespaceURI == null) { + const tag = type.toLowerCase(); if (tag === 'script') { // Create the script via .innerHTML so its "parser-inserted" flag is // set to true and it does not execute @@ -499,17 +508,17 @@ var ReactDOMFiberComponent = { var firstChild = ((div.firstChild : any) : HTMLScriptElement); domElement = div.removeChild(firstChild); } else if (props.is) { - domElement = ownerDocument.createElement(tag, props.is); + domElement = ownerDocument.createElement(type, props.is); } else { // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug. // See discussion in https://github.com/facebook/react/pull/6896 // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240 - domElement = ownerDocument.createElement(tag); + domElement = ownerDocument.createElement(type); } } else { domElement = ownerDocument.createElementNS( namespaceURI, - tag + type ); } diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js index fe8fc2d5ed..775d1b328c 100644 --- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js @@ -188,6 +188,39 @@ describe('ReactDOMFiber', () => { } if (ReactDOMFeatureFlags.useFiber) { + var svgEls, htmlEls, mathEls; + var expectSVG = {ref: el => svgEls.push(el)}; + var expectHTML = {ref: el => htmlEls.push(el)}; + var expectMath = {ref: el => mathEls.push(el)}; + + var portal = function(tree) { + return ReactDOM.unstable_createPortal( + tree, + document.createElement('div') + ); + }; + + var assertNamespacesMatch = function(tree) { + container = document.createElement('div'); + svgEls = []; + htmlEls = []; + mathEls = []; + + ReactDOM.render(tree, container); + svgEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + }); + htmlEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + }); + mathEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML'); + }); + + ReactDOM.unmountComponentAtNode(container); + expect(container.innerHTML).toBe(''); + }; + it('should render one portal', () => { var portalContainer = document.createElement('div'); @@ -333,6 +366,265 @@ describe('ReactDOMFiber', () => { expect(container.innerHTML).toBe(''); }); + it('should keep track of namespace across portals (simple)', () => { + assertNamespacesMatch( + + + {portal( +
+ )} + + + ); + assertNamespacesMatch( + + + {portal( +
+ )} + + + ); + assertNamespacesMatch( +
+

+ {portal( + + + + )} +

+

+ ); + }); + + it('should keep track of namespace across portals (medium)', () => { + assertNamespacesMatch( +
+ + + {portal( + + + + )} + +

+

+ ); + assertNamespacesMatch( + + + {portal( + + + +

+ + + +

+ + + + )} + + + ); + assertNamespacesMatch( +

+ {portal( + + {portal( +
+ )} + + + )} +

+

+ ); + assertNamespacesMatch( + + + {portal( +
+ )} + + + + + ); + }); + + it('should keep track of namespace across portals (complex)', () => { + assertNamespacesMatch( +
+ {portal( + + + + )} +

+ + + + + + + + + +

+

+ ); + assertNamespacesMatch( +
+ + + + {portal( + + + + + + + + )} + + +

+ {portal(

)} +

+ + + + +

+ + ); + assertNamespacesMatch( +

+ + +

+ {portal( + + + + + +

+ + {portal(

)} + + + + )} +

+ + + +

+ + ); + }); + + it('should unwind namespaces on uncaught errors', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + expect(() => { + assertNamespacesMatch( + + + + ); + }).toThrow('Hello'); + assertNamespacesMatch( +

+ ); + }); + + it('should unwind namespaces on caught errors', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return

; + } + return this.props.children; + } + } + + assertNamespacesMatch( + + + + + + + + + + + ); + assertNamespacesMatch( +

+ ); + }); + + it('should unwind namespaces on caught errors in a portal', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } + } + + assertNamespacesMatch( + + + {portal( +
+ + ) + +
+ )} + + { + /* + * TODO: enable. Currently this leads to stack overflow + * but it might be a bug in error boundaries rather than SVG or portals. + portal( +
+ ) + */ + } + + ); + }); + it('should pass portal context when rendering subtree elsewhere', () => { var portalContainer = document.createElement('div'); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js b/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js index 30760d3ae8..fd50a20d7e 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js @@ -12,12 +12,14 @@ 'use strict'; var React; +var ReactDOM; var ReactDOMServer; describe('ReactDOMSVG', () => { beforeEach(() => { React = require('React'); + ReactDOM = require('ReactDOM'); ReactDOMServer = require('ReactDOMServer'); }); @@ -30,4 +32,129 @@ describe('ReactDOMSVG', () => { expect(markup).toContain('xlink:href="http://i.imgur.com/w7GCRPb.png"'); }); + it('creates elements with SVG namespace inside SVG tag during mount', () => { + var node = document.createElement('div'); + var div, div2, div3, foreignObject, foreignObject2, g, image, image2, image3, p, svg, svg2, svg3, svg4; + ReactDOM.render( +
+ svg = el}> + g = el} strokeWidth="5"> + svg2 = el}> + foreignObject = el}> + svg3 = el}> + svg4 = el} /> + image = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + +
div = el} /> + + + image2 = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + foreignObject2 = el}> +
div2 = el} /> + + + +

p = el}> + + image3 = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + +

+
div3 = el} /> +
, + node + ); + [svg, svg2, svg3, svg4].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + // SVG tagName is case sensitive. + expect(el.tagName).toBe('svg'); + }); + expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(g.tagName).toBe('g'); + expect(g.getAttribute('stroke-width')).toBe('5'); + expect(p.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + // DOM tagName is capitalized by browsers. + expect(p.tagName).toBe('P'); + [image, image2, image3].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('image'); + expect( + el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + }); + [foreignObject, foreignObject2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('foreignObject'); + }); + [div, div2, div3].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(el.tagName).toBe('DIV'); + }); + }); + + it('creates elements with SVG namespace inside SVG tag during update', () => { + var inst, div, div2, foreignObject, foreignObject2, g, image, image2, svg, svg2, svg3, svg4; + + class App extends React.Component { + state = {step: 0}; + render() { + inst = this; + const {step} = this.state; + if (step === 0) { + return null; + } + return ( + g = el} strokeWidth="5"> + svg2 = el}> + foreignObject = el}> + svg3 = el}> + svg4 = el} /> + image = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + +
div = el} /> + + + image2 = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + foreignObject2 = el}> +
div2 = el} /> + + + ); + } + } + + var node = document.createElement('div'); + ReactDOM.render( + svg = el}> + + , + node + ); + inst.setState({step: 1}); + + [svg, svg2, svg3, svg4].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + // SVG tagName is case sensitive. + expect(el.tagName).toBe('svg'); + }); + expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(g.tagName).toBe('g'); + expect(g.getAttribute('stroke-width')).toBe('5'); + [image, image2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('image'); + expect( + el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + }); + [foreignObject, foreignObject2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('foreignObject'); + }); + [div, div2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + // DOM tagName is capitalized by browsers. + expect(el.tagName).toBe('DIV'); + }); + }); + }); diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 1e2435aa62..a365648f34 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -40,6 +40,10 @@ var instanceCounter = 0; var NoopRenderer = ReactFiberReconciler({ + getChildHostContext() { + return null; + }, + createInstance(type : string, props : Props) : Instance { const inst = { id: instanceCounter++, @@ -56,7 +60,7 @@ var NoopRenderer = ReactFiberReconciler({ parentInstance.children.push(child); }, - finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void { + finalizeInitialChildren(domElement : Instance, props : Props) : void { // Noop }, diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 3b442c5002..ca68c0d01b 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 { HostContext } from 'ReactFiberHostContext'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; import type { PriorityLevel } from 'ReactPriorityLevel'; @@ -59,13 +60,20 @@ var { var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactFiberClassComponent = require('ReactFiberClassComponent'); -module.exports = function( - config : HostConfig, +module.exports = function( + config : HostConfig, + hostContext : HostContext, scheduleUpdate : (fiber: Fiber) => void ) { const { shouldSetTextContent } = config; + const { + pushHostContext, + pushHostContainer, + resetHostContainer, + } = hostContext; + const { adoptClassInstance, constructClassInstance, @@ -233,7 +241,6 @@ module.exports = function( // empty, we need to schedule the text content to be reset. workInProgress.effectTag |= ContentReset; } - if (nextProps.hidden && workInProgress.pendingWorkPriority !== OffscreenPriority) { // If this host component is hidden, we can bail out on the children. @@ -270,6 +277,7 @@ module.exports = function( // Abort and don't process children yet. return null; } else { + pushHostContext(workInProgress); reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; } @@ -357,8 +365,9 @@ module.exports = function( function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber { const priorityLevel = workInProgress.pendingWorkPriority; + const isHostComponent = workInProgress.tag === HostComponent; - if (workInProgress.tag === HostComponent && + if (isHostComponent && workInProgress.memoizedProps.hidden && workInProgress.pendingWorkPriority !== OffscreenPriority) { // This subtree still has work, but it should be deprioritized so we need @@ -400,14 +409,32 @@ module.exports = function( cloneChildFibers(current, workInProgress); markChildAsProgressed(current, workInProgress, priorityLevel); + // Put context on the stack because we will work on children - if (isContextProvider(workInProgress)) { - pushContextProvider(workInProgress, false); + if (isHostComponent) { + pushHostContext(workInProgress); + } else { + switch (workInProgress.tag) { + case ClassComponent: + if (isContextProvider(workInProgress)) { + pushContextProvider(workInProgress, false); + } + break; + case HostRoot: + case HostPortal: + pushHostContainer(workInProgress.stateNode.containerInfo); + break; + } } + // TODO: this is annoyingly duplicating non-jump codepaths. + return workInProgress.child; } function bailoutOnLowPriority(current, workInProgress) { + if (workInProgress.tag === HostPortal) { + pushHostContainer(workInProgress.stateNode.containerInfo); + } // TODO: What if this is currently in progress? // How can that happen? How is this not being cloned? return null; @@ -417,6 +444,7 @@ module.exports = function( if (!workInProgress.return) { // Don't start new work with context on the stack. resetContext(); + resetHostContainer(); } if (workInProgress.pendingWorkPriority === NoWork || @@ -461,6 +489,7 @@ module.exports = function( } else { pushTopLevelContextObject(root.context, false); } + pushHostContainer(workInProgress.stateNode.containerInfo); reconcileChildren(current, workInProgress, workInProgress.pendingProps); // A yield component is just a placeholder, we can just run through the // next one immediately. @@ -486,8 +515,8 @@ module.exports = function( // next one immediately. return null; case HostPortal: + pushHostContainer(workInProgress.stateNode.containerInfo); updatePortalComponent(current, workInProgress); - // TODO: is this right? return workInProgress.child; case Fragment: updateFragment(current, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 539cc41002..5e31b11fcc 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -13,6 +13,7 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; +import type { HostContext } from 'ReactFiberHostContext'; import type { HostConfig } from 'ReactFiberReconciler'; var ReactTypeOfWork = require('ReactTypeOfWork'); @@ -33,18 +34,24 @@ var { ContentReset, } = require('ReactTypeOfSideEffect'); -module.exports = function( - config : HostConfig, +module.exports = function( + config : HostConfig, + hostContext : HostContext, captureError : (failedFiber : Fiber, error: Error) => ?Fiber ) { - const commitUpdate = config.commitUpdate; - const resetTextContent = config.resetTextContent; - const commitTextUpdate = config.commitTextUpdate; + const { + commitUpdate, + resetTextContent, + commitTextUpdate, + appendChild, + insertBefore, + removeChild, + } = config; - const appendChild = config.appendChild; - const insertBefore = config.insertBefore; - const removeChild = config.removeChild; + const { + getRootHostContainer, + } = hostContext; // Capture errors so they don't interrupt unmounting. function safelyCallComponentWillUnmount(current, instance) { @@ -354,7 +361,8 @@ module.exports = function( // Commit the work prepared earlier. const newProps = finishedWork.memoizedProps; const oldProps = current.memoizedProps; - commitUpdate(instance, oldProps, newProps, finishedWork); + const rootContainerInstance = getRootHostContainer(); + commitUpdate(instance, oldProps, newProps, rootContainerInstance, finishedWork); } detachRefIfNeeded(current, finishedWork); return; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index f683a08331..a4a0143069 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -14,6 +14,7 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; +import type { HostContext } from 'ReactFiberHostContext'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; import type { ReifiedYield } from 'ReactReifiedYield'; @@ -43,13 +44,24 @@ var { Callback, } = ReactTypeOfSideEffect; -module.exports = function(config : HostConfig) { +module.exports = function( + config : HostConfig, + hostContext : HostContext, +) { + const { + createInstance, + createTextInstance, + appendInitialChild, + finalizeInitialChildren, + prepareUpdate, + } = config; - const createInstance = config.createInstance; - const appendInitialChild = config.appendInitialChild; - const finalizeInitialChildren = config.finalizeInitialChildren; - const createTextInstance = config.createTextInstance; - const prepareUpdate = config.prepareUpdate; + const { + getRootHostContainer, + popHostContext, + getHostContext, + popHostContainer, + } = hostContext; function markUpdate(workInProgress : Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -202,6 +214,7 @@ module.exports = function(config : HostConfig) { return null; } case HostComponent: + popHostContext(workInProgress); let newProps = workInProgress.pendingProps; if (current && workInProgress.stateNode != null) { // If we have an alternate, that means this is an update and we need to @@ -229,14 +242,21 @@ module.exports = function(config : HostConfig) { } } + const rootContainerInstance = getRootHostContainer(); + const currentHostContext = getHostContext(); // TODO: Move createInstance to beginWork and keep it on a context // "stack" as the parent. Then append children as we go in beginWork // or completeWork depending on we want to add then top->down or // bottom->up. Top->down is faster in IE11. - // Finally, finalizeInitialChildren here in completeWork. - const instance = createInstance(workInProgress.type, newProps, workInProgress); + const instance = createInstance( + workInProgress.type, + newProps, + rootContainerInstance, + currentHostContext, + workInProgress + ); appendAllChildren(instance, workInProgress); - finalizeInitialChildren(instance, workInProgress.type, newProps); + finalizeInitialChildren(instance, newProps, rootContainerInstance); workInProgress.stateNode = instance; if (workInProgress.ref) { @@ -294,6 +314,7 @@ module.exports = function(config : HostConfig) { // TODO: Only mark this as an update if we have any pending callbacks. markUpdate(workInProgress); workInProgress.memoizedProps = workInProgress.pendingProps; + popHostContainer(); return null; // Error cases diff --git a/src/renderers/shared/fiber/ReactFiberHostContext.js b/src/renderers/shared/fiber/ReactFiberHostContext.js new file mode 100644 index 0000000000..5a8e580a12 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberHostContext.js @@ -0,0 +1,157 @@ +/** + * 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 ReactFiberHostContext + * @flow + */ + +'use strict'; + +import type { Fiber } from 'ReactFiber'; +import type { HostConfig } from 'ReactFiberReconciler'; + +export type HostContext = { + getRootHostContainer() : C, + getHostContext() : CX | null, + + pushHostContext(fiber : Fiber) : void, + popHostContext(fiber : Fiber) : void, + + pushHostContainer(container : C) : void, + popHostContainer() : void, + resetHostContainer() : void, +}; + +module.exports = function( + config : HostConfig +) : HostContext { + const { + getChildHostContext, + } = config; + + type ContainerState = { + rootInstance : C, + contextFibers : Array | null, + contextValues : Array | null, + contextDepth: number + }; + + function createContainerState(rootInstance) { + return { + rootInstance, + contextFibers: null, + contextValues: null, + contextDepth: -1, + }; + } + + // If we meet any portals, we'll go deeper. + let containerStack : Array = []; + let containerDepth : number = -1; + + function getRootHostContainer() : C { + if (containerDepth === -1) { + throw new Error('Expected to find a root container.'); + } + const containerState = containerStack[containerDepth]; + const {rootInstance} = containerState; + return rootInstance; + } + + function pushHostContainer(portalHostContainer) { + containerDepth++; + if (containerDepth === containerStack.length) { + containerStack[containerDepth] = createContainerState(portalHostContainer); + } else { + const containerState = containerStack[containerDepth]; + containerState.rootInstance = portalHostContainer; + containerState.contextFibers = null; + containerState.contextValues = null; + containerState.contextDepth = -1; + } + } + + function popHostContainer() { + if (containerDepth === -1) { + throw new Error('Already reached the root.'); + } + containerDepth--; + } + + function getHostContext() : CX | null { + if (containerDepth == null) { + throw new Error('Expected to find a root container.'); + } + const containerState = containerStack[containerDepth]; + const {contextDepth, contextValues} = containerState; + if (contextDepth === -1) { + return null; + } + if (contextValues == null) { + throw new Error('Expected context values to exist.'); + } + return contextValues[contextDepth]; + } + + function pushHostContext(fiber : Fiber) : void { + const parentHostContext = getHostContext(); + const currentHostContext = getChildHostContext(parentHostContext, fiber.type); + if (parentHostContext === currentHostContext) { + return; + } + const containerState = containerStack[containerDepth]; + let {contextDepth, contextFibers, contextValues} = containerState; + if (contextFibers == null) { + contextFibers = []; + containerState.contextFibers = contextFibers; + } + if (contextValues == null) { + contextValues = []; + containerState.contextValues = contextValues; + } + contextDepth++; + containerState.contextDepth = contextDepth; + contextFibers[contextDepth] = fiber; + contextValues[contextDepth] = currentHostContext; + } + + function popHostContext(fiber : Fiber) : void { + const containerState = containerStack[containerDepth]; + let {contextDepth} = containerState; + if (contextDepth === -1) { + return; + } + const {contextFibers, contextValues} = containerState; + if (contextFibers == null || contextValues == null) { + throw new Error('Expected host context stacks to exist when index is more than -1.'); + } + if (fiber !== contextFibers[contextDepth]) { + return; + } + contextFibers[contextDepth] = null; + contextValues[contextDepth] = null; + contextDepth--; + containerState.contextDepth = contextDepth; + } + + function resetHostContainer() { + containerDepth = -1; + } + + return { + getRootHostContainer, + getHostContext, + + pushHostContext, + popHostContext, + + pushHostContainer, + popHostContainer, + resetHostContainer, + }; +}; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 40466f814f..f3b894d924 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -40,14 +40,16 @@ export type Deadline = { type OpaqueNode = Fiber; -export type HostConfig = { +export type HostConfig = { - createInstance(type : T, props : P, internalInstanceHandle : OpaqueNode) : I, - appendInitialChild(parentInstance : I, child : I) : void, - finalizeInitialChildren(parentInstance : I, type : T, props : P) : void, + getChildHostContext(parentHostContext : CX | null, type : T) : CX, + + createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX | null, internalInstanceHandle : OpaqueNode) : I, + appendInitialChild(parentInstance : I, child : I | TI) : void, + finalizeInitialChildren(parentInstance : I, props : P, rootContainerInstance : C) : void, prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean, - commitUpdate(instance : I, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void, + commitUpdate(instance : I, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void, shouldSetTextContent(props : P) : boolean, resetTextContent(instance : I) : void, @@ -93,7 +95,7 @@ getContextForSubtree._injectFiber(function(fiber : Fiber) { parentContext; }); -module.exports = function(config : HostConfig) : Reconciler { +module.exports = function(config : HostConfig) : Reconciler { var { scheduleWork, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index c6ee0d5dc0..4d50ac0a41 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -20,6 +20,7 @@ import type { PriorityLevel } from 'ReactPriorityLevel'; var ReactFiberBeginWork = require('ReactFiberBeginWork'); var ReactFiberCompleteWork = require('ReactFiberCompleteWork'); var ReactFiberCommitWork = require('ReactFiberCommitWork'); +var ReactFiberHostContext = require('ReactFiberHostContext'); var ReactCurrentOwner = require('ReactCurrentOwner'); var { cloneFiber } = require('ReactFiber'); @@ -47,6 +48,8 @@ var { var { HostRoot, + HostComponent, + HostPortal, ClassComponent, } = require('ReactTypeOfWork'); @@ -60,19 +63,25 @@ if (__DEV__) { var timeHeuristicForUnitOfWork = 1; -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { + const hostContext = ReactFiberHostContext(config); + const { popHostContainer, popHostContext, resetHostContainer } = hostContext; const { beginWork, beginFailedWork } = - ReactFiberBeginWork(config, scheduleUpdate); - const { completeWork } = ReactFiberCompleteWork(config); - const { commitPlacement, commitDeletion, commitWork, commitLifeCycles } = - ReactFiberCommitWork(config, captureError); - - const hostScheduleAnimationCallback = config.scheduleAnimationCallback; - const hostScheduleDeferredCallback = config.scheduleDeferredCallback; - const useSyncScheduling = config.useSyncScheduling; - - const prepareForCommit = config.prepareForCommit; - const resetAfterCommit = config.resetAfterCommit; + ReactFiberBeginWork(config, hostContext, scheduleUpdate); + const { completeWork } = ReactFiberCompleteWork(config, hostContext); + const { + commitPlacement, + commitDeletion, + commitWork, + commitLifeCycles, + } = ReactFiberCommitWork(config, hostContext, captureError); + const { + scheduleAnimationCallback: hostScheduleAnimationCallback, + scheduleDeferredCallback: hostScheduleDeferredCallback, + useSyncScheduling, + prepareForCommit, + resetAfterCommit, + } = config; // The priority level to use when scheduling an update. let priorityContext : PriorityLevel = useSyncScheduling ? @@ -309,6 +318,9 @@ module.exports = function(config : HostConfig) { } resetAfterCommit(); + // We didn't pop the host root in the complete phase because we still needed + // it for the commitUpdate() calls, but now we can reset host context. + resetHostContainer(); // In the second pass we'll perform all life-cycles and ref callbacks. // Life-cycles happen as a separate pass so that all placements, updates, @@ -658,6 +670,7 @@ module.exports = function(config : HostConfig) { // props, the nodes higher up in the tree will rerender unnecessarily. if (failedWork) { unwindContext(failedWork, boundary); + unwindHostContext(failedWork, boundary); } nextUnitOfWork = completeUnitOfWork(boundary); } @@ -864,6 +877,24 @@ module.exports = function(config : HostConfig) { } } + function unwindHostContext(from : Fiber, to: Fiber) { + let node = from; + while (node && (node !== to) && (node.alternate !== to)) { + switch (node.tag) { + case HostComponent: + popHostContext(node); + break; + case HostRoot: + popHostContainer(); + break; + case HostPortal: + popHostContainer(); + break; + } + node = node.return; + } + } + function scheduleWork(root : FiberRoot) { let priorityLevel = priorityContext;