Added new fiber-based renderer for native dubbed ReactNativeFiber

This commit is contained in:
Brian Vaughn
2016-12-07 18:15:13 -08:00
parent ac241972bd
commit 5266ca9312
13 changed files with 650 additions and 82 deletions

View File

@@ -29,16 +29,35 @@ declare module 'TextInputState' {
declare module 'UIManager' {
declare var customBubblingEventTypes : Object;
declare var customDirectEventTypes : Object;
declare function createView() : void;
declare function manageChildren() : void;
declare function createView(
reactTag : number,
viewName : string,
rootTag : number,
props : ?Object,
) : void;
declare function manageChildren(
containerTag : number,
moveFromIndices : Array<number>,
moveToIndices : Array<number>,
addChildReactTags : Array<number>,
addAtIndices : Array<number>,
removeAtIndices : Array<number>
) : void;
declare function measure() : void;
declare function measureInWindow() : void;
declare function measureLayout() : void;
declare function removeRootView() : void;
declare function removeSubviewsFromContainerWithID() : void;
declare function replaceExistingNonRootView() : void;
declare function setChildren() : void;
declare function updateView() : void;
declare function setChildren(
containerTag : number,
reactTags : Array<number>,
) : void;
declare function updateView(
reactTag : number,
viewName : string,
props : ?Object,
) : void;
}
declare module 'View' {
declare var exports : typeof ReactComponent;

View File

@@ -150,7 +150,7 @@ var NativeMethodsMixin = {
);
UIManager.updateView(
findNodeHandle(this),
(findNodeHandle(this) : any),
this.viewConfig.uiViewClassName,
updatePayload
);

View File

@@ -11,67 +11,5 @@
*/
'use strict';
// Require ReactNativeDefaultInjection first for its side effects of setting up
// the JS environment
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactNativeInjection = require('ReactNativeInjection');
var ReactNativeStackInjection = require('ReactNativeStackInjection');
var ReactNativeMount = require('ReactNativeMount');
var ReactUpdates = require('ReactUpdates');
var findNodeHandle = require('findNodeHandle');
ReactNativeInjection.inject();
ReactNativeStackInjection.inject();
var render = function(
element: ReactElement<any>,
mountInto: number,
callback?: ?(() => void)
): ?ReactComponent<any, any, any> {
return ReactNativeMount.renderComponent(element, mountInto, callback);
};
var ReactNative = {
hasReactNativeInitialized: false,
findNodeHandle: findNodeHandle,
render: render,
unmountComponentAtNode: ReactNativeMount.unmountComponentAtNode,
/* eslint-disable camelcase */
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
/* eslint-enable camelcase */
unmountComponentAtNodeAndRemoveContainer: ReactNativeMount.unmountComponentAtNodeAndRemoveContainer,
};
// Inject the runtime into a devtools global hook regardless of browser.
// Allows for debugging when the hook is injected on the page.
/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
ComponentTree: {
getClosestInstanceFromNode: function(node) {
return ReactNativeComponentTree.getClosestInstanceFromNode(node);
},
getNodeFromInstance: function(inst) {
// inst is an internal instance (but could be a composite)
while (inst._renderedComponent) {
inst = inst._renderedComponent;
}
if (inst) {
return ReactNativeComponentTree.getNodeFromInstance(inst);
} else {
return null;
}
},
},
Mount: ReactNativeMount,
Reconciler: require('ReactReconciler'),
});
}
module.exports = ReactNative;
// TODO (bvaughn) Enable Fiber experiement via ReactNativeFeatureFlags
module.exports = require('ReactNativeStack');

View File

@@ -39,6 +39,10 @@ function precacheNode(inst, tag) {
instanceCache[tag] = nativeInst;
}
function precacheFiberNode(hostInst, tag) {
instanceCache[tag] = hostInst;
}
function uncacheNode(inst) {
var tag = inst._rootNodeID;
if (tag) {
@@ -46,21 +50,29 @@ function uncacheNode(inst) {
}
}
function uncacheFiberNode(tag) {
delete instanceCache[tag];
}
function getInstanceFromTag(tag) {
return instanceCache[tag] || null;
}
function getTagFromInstance(inst) {
invariant(inst._rootNodeID, 'All native instances should have a tag.');
return inst._rootNodeID;
// TODO (bvaughn) Clean up once Stack is deprecated
var tag = inst._rootNodeID || inst.stateNode._nativeTag;
invariant(tag, 'All native instances should have a tag.');
return tag;
}
var ReactNativeComponentTree = {
getClosestInstanceFromNode: getInstanceFromTag,
getInstanceFromNode: getInstanceFromTag,
getNodeFromInstance: getTagFromInstance,
precacheNode: precacheNode,
uncacheNode: uncacheNode,
precacheFiberNode,
precacheNode,
uncacheFiberNode,
uncacheNode,
};
module.exports = ReactNativeComponentTree;

View File

@@ -0,0 +1,18 @@
/**
* 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 ReactNativeFeatureFlags
*/
'use strict';
var ReactNativeFeatureFlags = {
useFiber: false,
};
module.exports = ReactNativeFeatureFlags;

View File

@@ -0,0 +1,395 @@
/**
* 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 ReactNativeFiber
* @flow
*/
'use strict';
import type { Element } from 'React';
import type { Fiber } from 'ReactFiber';
import type { ReactNodeList } from 'ReactTypes';
import type { ReactNativeBaseComponentViewConfig } from 'ReactNativeViewConfigRegistry';
const NativeMethodsMixin = require('NativeMethodsMixin');
const ReactFiberReconciler = require('ReactFiberReconciler');
const ReactGenericBatching = require('ReactGenericBatching');
const ReactNativeAttributePayload = require('ReactNativeAttributePayload');
const ReactNativeComponentTree = require('ReactNativeComponentTree');
const ReactNativeInjection = require('ReactNativeInjection');
const ReactNativeTagHandles = require('ReactNativeTagHandles');
const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry');
const ReactPortal = require('ReactPortal');
const UIManager = require('UIManager');
const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
const findNodeHandle = require('findNodeHandle');
const invariant = require('invariant');
const { precacheFiberNode, uncacheFiberNode } = ReactNativeComponentTree;
ReactNativeInjection.inject();
type Container = number;
type Instance = {
_children: Array<Instance | number>,
_nativeTag: number,
viewConfig: ReactNativeBaseComponentViewConfig,
};
type Props = Object;
type TextInstance = number;
function NativeHostComponent(tag, viewConfig) {
this._nativeTag = tag;
this._children = [];
this.viewConfig = viewConfig;
}
Object.assign(NativeHostComponent.prototype, NativeMethodsMixin);
function recursivelyUncacheFiberNode(node : Instance | TextInstance) {
if (typeof node === 'number') { // Leaf node (eg text)
uncacheFiberNode(node);
} else {
uncacheFiberNode((node : any)._nativeTag);
(node : any)._children.forEach(recursivelyUncacheFiberNode);
}
}
const NativeRenderer = ReactFiberReconciler({
appendChild(
parentInstance : Instance | Container,
child : Instance | TextInstance
) : void {
if (typeof parentInstance === 'number') {
// Root container
UIManager.setChildren(
parentInstance, // containerTag
[(child : any)._nativeTag] // reactTags
);
} else {
const children = parentInstance._children;
children.push(child);
UIManager.manageChildren(
parentInstance._nativeTag, // containerTag
[], // moveFromIndices
[], // moveToIndices
[(child : any)._nativeTag], // addChildReactTags
[children.length - 1], // addAtIndices
[], // removeAtIndices
);
}
},
appendInitialChild(parentInstance : Instance, child : Instance | TextInstance) : void {
if (typeof child === 'number') {
parentInstance._children.push(child);
} else {
parentInstance._children.push(child);
}
},
commitTextUpdate(
textInstance : TextInstance,
oldText : string,
newText : string
) : void {
UIManager.updateView(
textInstance, // reactTag
'RCTRawText', // viewName
{text: newText}, // props
);
},
commitUpdate(
instance : Instance,
type : string,
oldProps : Props,
newProps : Props,
rootContainerInstance : Object,
internalInstanceHandle : Object
) : void {
const viewConfig = instance.viewConfig;
precacheFiberNode(internalInstanceHandle, instance._nativeTag);
const updatePayload = ReactNativeAttributePayload.diff(
oldProps,
newProps,
viewConfig.validAttributes
);
UIManager.updateView(
(instance : any)._nativeTag, // reactTag
viewConfig.uiViewClassName, // viewName
updatePayload, // props
);
},
createInstance(
type : string,
props : Props,
rootContainerInstance : Container,
hostContext : string | null,
internalInstanceHandle : Object
) : Instance {
const tag = ReactNativeTagHandles.allocateTag();
const viewConfig = ReactNativeViewConfigRegistry.get(type);
if (__DEV__) {
for (let key in viewConfig.validAttributes) {
if (props.hasOwnProperty(key)) {
deepFreezeAndThrowOnMutationInDev(props[key]);
}
}
}
const updatePayload = ReactNativeAttributePayload.create(
props,
viewConfig.validAttributes
);
UIManager.createView(
tag, // reactTag
viewConfig.uiViewClassName, // viewName
rootContainerInstance, // rootTag
updatePayload, // props
);
const component = new NativeHostComponent(tag, viewConfig);
precacheFiberNode(internalInstanceHandle, tag);
return component;
},
createTextInstance(
text : string,
rootContainerInstance : Container,
internalInstanceHandle : Object,
) : TextInstance {
const tag = ReactNativeTagHandles.allocateTag();
UIManager.createView(
tag, // reactTag
'RCTRawText', // viewName
rootContainerInstance, // rootTag
{text: text} // props
);
precacheFiberNode(internalInstanceHandle, tag);
return tag;
},
finalizeInitialChildren(
parentInstance : Instance,
type : string,
props : Props,
rootContainerInstance : Container,
) : void {
// Map from child objects to native tags.
// Either way we need to pass a copy of the Array to prevent it from being frozen.
const nativeTags = parentInstance._children.map(
(child) => typeof child === 'number'
? child // Leaf node (eg text)
: child._nativeTag
);
UIManager.setChildren(
parentInstance._nativeTag, // containerTag
nativeTags // reactTags
);
},
getChildHostContext(
parentHostContext : string | null,
type : string
) {
return parentHostContext;
},
insertBefore(
parentInstance : Instance | Container,
child : Instance | TextInstance,
beforeChild : Instance | TextInstance
) : void {
// TODO (bvaughn): Remove this check when...
// We create a wrapper object for the container in ReactNative render()
// Or we refactor to remove wrapper objects entirely.
// For more info on pros/cons see PR #8560 description.
invariant(
typeof parentInstance !== 'number',
'Container does not support insertBefore operation',
);
const children = (parentInstance : any)._children;
const beforeChildIndex = children.indexOf(beforeChild);
const index = children.indexOf(child);
// Move existing child or add new child?
if (index >= 0) {
children.splice(index, 1);
children.splice(beforeChildIndex, 0, child);
UIManager.manageChildren(
(parentInstance : any)._nativeTag, // containerID
[index], // moveFromIndices
[beforeChildIndex], // moveToIndices
[], // addChildReactTags
[], // addAtIndices
[], // removeAtIndices
);
} else {
children.splice(beforeChildIndex, 0, child);
UIManager.manageChildren(
(parentInstance : any)._nativeTag, // containerID
[], // moveFromIndices
[], // moveToIndices
[(child : any)._nativeTag], // addChildReactTags
[beforeChildIndex], // addAtIndices
[], // removeAtIndices
);
}
},
prepareForCommit() : void {
// Noop
},
prepareUpdate(
instance : Instance,
type : string,
oldProps : Props,
newProps : Props
) : boolean {
return true;
},
removeChild(
parentInstance : Instance | Container,
child : Instance | TextInstance
) : void {
recursivelyUncacheFiberNode(child);
if (typeof parentInstance === 'number') {
UIManager.manageChildren(
parentInstance, // containerID
[], // moveFromIndices
[], // moveToIndices
[], // addChildReactTags
[], // addAtIndices
[0], // removeAtIndices
);
} else {
const children = parentInstance._children;
const index = children.indexOf(child);
children.splice(index, 1);
UIManager.manageChildren(
parentInstance._nativeTag, // containerID
[], // moveFromIndices
[], // moveToIndices
[], // addChildReactTags
[], // addAtIndices
[index], // removeAtIndices
);
}
},
resetAfterCommit() : void {
// Noop
},
resetTextContent(instance : Instance) : void {
// Noop
},
scheduleAnimationCallback: global.requestAnimationFrame,
scheduleDeferredCallback: global.requestIdleCallback,
shouldSetTextContent(props : Props) : boolean {
// TODO (bvaughn) Revisit this decision.
// Always returning false simplifies the createInstance() implementation,
// But creates an additional child Fiber for raw text children.
// No additional native views are created though.
// It's not clear to me which is better so I'm deferring for now.
// More context @ github.com/facebook/react/pull/8560#discussion_r92111303
return false;
},
useSyncScheduling: true,
});
ReactGenericBatching.injection.injectFiberBatchedUpdates(
NativeRenderer.batchedUpdates
);
const roots = new Map();
findNodeHandle.injection.injectFindNode(
(fiber: Fiber) => {
const instance: any = NativeRenderer.findHostInstance(fiber);
return instance ? instance._nativeTag : null;
}
);
findNodeHandle.injection.injectFindRootNodeID(
(instance) => instance._nativeTag
);
const ReactNative = {
findNodeHandle,
render(element : Element<any>, containerTag : any, callback: ?Function) {
let root = roots.get(containerTag);
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = NativeRenderer.mountContainer(element, containerTag, null, callback);
roots.set(containerTag, root);
} else {
NativeRenderer.updateContainer(element, root, null, callback);
}
return NativeRenderer.getPublicRootInstance(root);
},
unmountComponentAtNode(containerTag : number) {
const root = roots.get(containerTag);
if (root) {
// TODO: Is it safe to reset this now or should I wait since this unmount could be deferred?
roots.delete(containerTag);
NativeRenderer.unmountContainer(root);
}
},
unmountComponentAtNodeAndRemoveContainer(containerTag: number) {
ReactNative.unmountComponentAtNode(containerTag);
// Call back into native to remove all of the subviews from this container
UIManager.removeRootView(containerTag);
},
unstable_createPortal(children: ReactNodeList, containerTag : number, key : ?string = null) {
return ReactPortal.createPortal(children, containerTag, null, key);
},
unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
};
module.exports = ReactNative;

View File

@@ -15,8 +15,9 @@ var UIManager = require('UIManager');
var ReactNativeGlobalResponderHandler = {
onChange: function(from, to, blockNativeResponder) {
if (to !== null) {
// TODO (bvaughn) Clean up once Stack is deprecated
UIManager.setJSResponder(
to._rootNodeID,
to._rootNodeID || to.stateNode._nativeTag,
blockNativeResponder
);
} else {

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeStack
* @flow
*/
'use strict';
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactNativeInjection = require('ReactNativeInjection');
var ReactNativeStackInjection = require('ReactNativeStackInjection');
var ReactNativeMount = require('ReactNativeMount');
var ReactUpdates = require('ReactUpdates');
var findNodeHandle = require('findNodeHandle');
ReactNativeInjection.inject();
ReactNativeStackInjection.inject();
var render = function(
element: ReactElement<any>,
mountInto: number,
callback?: ?(() => void)
): ?ReactComponent<any, any, any> {
return ReactNativeMount.renderComponent(element, mountInto, callback);
};
findNodeHandle.injection.injectFindNode(
(instance) => instance.getHostNode()
);
findNodeHandle.injection.injectFindRootNodeID(
(instance) => instance._rootNodeID
);
var ReactNative = {
hasReactNativeInitialized: false,
findNodeHandle: findNodeHandle,
render: render,
unmountComponentAtNode: ReactNativeMount.unmountComponentAtNode,
/* eslint-disable camelcase */
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
/* eslint-enable camelcase */
unmountComponentAtNodeAndRemoveContainer: ReactNativeMount.unmountComponentAtNodeAndRemoveContainer,
};
// Inject the runtime into a devtools global hook regardless of browser.
// Allows for debugging when the hook is injected on the page.
/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
ComponentTree: {
getClosestInstanceFromNode: function(node) {
return ReactNativeComponentTree.getClosestInstanceFromNode(node);
},
getNodeFromInstance: function(inst) {
// inst is an internal instance (but could be a composite)
while (inst._renderedComponent) {
inst = inst._renderedComponent;
}
if (inst) {
return ReactNativeComponentTree.getNodeFromInstance(inst);
} else {
return null;
}
},
},
Mount: ReactNativeMount,
Reconciler: require('ReactReconciler'),
});
}
module.exports = ReactNative;

View File

@@ -30,6 +30,7 @@ var ReactNativeTextComponent = require('ReactNativeTextComponent');
var ReactSimpleEmptyComponent = require('ReactSimpleEmptyComponent');
var ReactUpdates = require('ReactUpdates');
var findNodeHandle = require('findNodeHandle');
var invariant = require('invariant');
function inject() {
@@ -61,6 +62,13 @@ function inject() {
);
};
findNodeHandle.injection.injectFindNode(
(instance) => instance.getHostNode()
);
findNodeHandle.injection.injectFindRootNodeID(
(instance) => instance._rootNodeID
);
ReactEmptyComponent.injection.injectEmptyComponentFactory(EmptyComponent);
ReactHostComponent.injection.injectTextComponentClass(

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeViewConfigRegistry
* @flow
*/
'use strict';
const invariant = require('invariant');
export type ReactNativeBaseComponentViewConfig = {
validAttributes: Object,
uiViewClassName: string,
propTypes?: Object,
};
const viewConfigs = new Map();
const prefix = 'topsecret-';
const ReactNativeViewConfigRegistry = {
register(viewConfig : ReactNativeBaseComponentViewConfig) {
const name = viewConfig.uiViewClassName;
invariant(
!viewConfigs.has(name),
'Tried to register two views with the same name %s',
name
);
const secretName = prefix + name;
viewConfigs.set(secretName, viewConfig);
return secretName;
},
get(secretName: string) {
const config = viewConfigs.get(secretName);
invariant(
config,
'View config not found for name %s',
secretName
);
return config;
},
};
module.exports = ReactNativeViewConfigRegistry;

View File

@@ -0,0 +1,16 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const ReactNativeFeatureFlags = require('ReactNativeFeatureFlags');
module.exports = ReactNativeFeatureFlags.useFiber
? require('ReactNativeFiber')
: require('ReactNativeStack');

View File

@@ -12,23 +12,35 @@
'use strict';
var ReactNativeBaseComponent = require('ReactNativeBaseComponent');
const ReactNativeBaseComponent = require('ReactNativeBaseComponent');
const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry');
const ReactNativeFeatureFlags = require('ReactNativeFeatureFlags');
// See also ReactNativeBaseComponent
type ReactNativeBaseComponentViewConfig = {
validAttributes: Object,
uiViewClassName: string,
propTypes?: Object,
}
};
/**
* @param {string} config iOS View configuration.
* @private
*/
var createReactNativeComponentClass = function(
const createReactNativeFiberComponentClass = function(
viewConfig: ReactNativeBaseComponentViewConfig
): string {
return ReactNativeViewConfigRegistry.register(viewConfig);
};
/**
* @param {string} config iOS View configuration.
* @private
*/
const createReactNativeComponentClass = function(
viewConfig: ReactNativeBaseComponentViewConfig
): ReactClass<any> {
var Constructor = function(element) {
const Constructor = function(element) {
this._currentElement = element;
this._topLevelWrapper = null;
this._hostParent = null;
@@ -45,4 +57,6 @@ var createReactNativeComponentClass = function(
return ((Constructor: any): ReactClass<any>);
};
module.exports = createReactNativeComponentClass;
module.exports = ReactNativeFeatureFlags.useFiber
? createReactNativeFiberComponentClass
: createReactNativeComponentClass;

View File

@@ -50,6 +50,9 @@ import type { ReactInstance } from 'ReactInstanceType';
* nodeHandle N/A rootNodeID tag
*/
let injectedFindNode;
let injectedFindRootNodeID;
function findNodeHandle(componentOrHandle: any): ?number {
if (__DEV__) {
// TODO: fix this unsafe cast to work with Fiber.
@@ -82,9 +85,9 @@ function findNodeHandle(componentOrHandle: any): ?number {
// ReactInstanceMap.get here will always succeed for mounted components
var internalInstance = ReactInstanceMap.get(component);
if (internalInstance) {
return internalInstance.getHostNode();
return injectedFindNode(internalInstance);
} else {
var rootNodeID = component._rootNodeID;
var rootNodeID = injectedFindRootNodeID(component);
if (rootNodeID) {
return rootNodeID;
} else {
@@ -92,7 +95,10 @@ function findNodeHandle(componentOrHandle: any): ?number {
(
// Native
typeof component === 'object' &&
'_rootNodeID' in component
(
'_rootNodeID' in component || // TODO (bvaughn) Clean up once Stack is deprecated
'_nativeTag' in component
)
) || (
// Composite
component.render != null &&
@@ -112,4 +118,14 @@ function findNodeHandle(componentOrHandle: any): ?number {
}
}
// Fiber and stack implementations differ; each must inject a strategy
findNodeHandle.injection = {
injectFindNode(findNode) {
injectedFindNode = findNode;
},
injectFindRootNodeID(findRootNodeID) {
injectedFindRootNodeID = findRootNodeID;
},
};
module.exports = findNodeHandle;