UNPKG

react-native-macos

Version:

A framework for building native macOS apps using React

872 lines (786 loc) • 30.9 kB
/** * 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 ReactFiberBeginWork * @flow */ 'use strict'; 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'; var { mountChildFibersInPlace, reconcileChildFibers, reconcileChildFibersInPlace, cloneChildFibers, } = require('ReactChildFiber'); var {beginUpdateQueue} = require('ReactFiberUpdateQueue'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { getMaskedContext, getUnmaskedContext, hasContextChanged, pushContextProvider, pushTopLevelContextObject, invalidateContextProvider, } = require('ReactFiberContext'); var { IndeterminateComponent, FunctionalComponent, ClassComponent, HostRoot, HostComponent, HostText, HostPortal, CoroutineComponent, CoroutineHandlerPhase, YieldComponent, Fragment, } = ReactTypeOfWork; var {NoWork, OffscreenPriority} = require('ReactPriorityLevel'); var {Placement, ContentReset, Err, Ref} = require('ReactTypeOfSideEffect'); var ReactFiberClassComponent = require('ReactFiberClassComponent'); var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var invariant = require('fbjs/lib/invariant'); if (__DEV__) { var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); var {cancelWorkTimer} = require('ReactDebugFiberPerf'); var warning = require('fbjs/lib/warning'); var warnedAboutStatelessRefs = {}; } module.exports = function<T, P, I, TI, PI, C, CX, PL>( config: HostConfig<T, P, I, TI, PI, C, CX, PL>, hostContext: HostContext<C, CX>, scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, ) { const { shouldSetTextContent, useSyncScheduling, shouldDeprioritizeSubtree, } = config; const {pushHostContext, pushHostContainer} = hostContext; const { adoptClassInstance, constructClassInstance, mountClassInstance, resumeMountClassInstance, updateClassInstance, } = ReactFiberClassComponent( scheduleUpdate, getPriorityContext, memoizeProps, memoizeState, ); function markChildAsProgressed(current, workInProgress, priorityLevel) { // We now have clones. Let's store them as the currently progressed work. workInProgress.progressedChild = workInProgress.child; workInProgress.progressedPriority = priorityLevel; if (current !== null) { // We also store it on the current. When the alternate swaps in we can // continue from this point. current.progressedChild = workInProgress.progressedChild; current.progressedPriority = workInProgress.progressedPriority; } } function clearDeletions(workInProgress) { workInProgress.progressedFirstDeletion = workInProgress.progressedLastDeletion = null; } function transferDeletions(workInProgress) { // Any deletions get added first into the effect list. workInProgress.firstEffect = workInProgress.progressedFirstDeletion; workInProgress.lastEffect = workInProgress.progressedLastDeletion; } function reconcileChildren(current, workInProgress, nextChildren) { const priorityLevel = workInProgress.pendingWorkPriority; reconcileChildrenAtPriority( current, workInProgress, nextChildren, priorityLevel, ); } function reconcileChildrenAtPriority( current, workInProgress, nextChildren, priorityLevel, ) { // At this point any memoization is no longer valid since we'll have changed // the children. workInProgress.memoizedProps = null; if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, // we will add them all to the child before it gets rendered. That means // we can optimize this reconciliation pass by not tracking side-effects. workInProgress.child = mountChildFibersInPlace( workInProgress, workInProgress.child, nextChildren, priorityLevel, ); } else if (current.child === workInProgress.child) { // If the current child is the same as the work in progress, it means that // we haven't yet started any work on these children. Therefore, we use // the clone algorithm to create a copy of all the current children. // If we had any progressed work already, that is invalid at this point so // let's throw it out. clearDeletions(workInProgress); workInProgress.child = reconcileChildFibers( workInProgress, workInProgress.child, nextChildren, priorityLevel, ); transferDeletions(workInProgress); } else { // If, on the other hand, it is already using a clone, that means we've // already begun some work on this tree and we can continue where we left // off by reconciling against the existing children. workInProgress.child = reconcileChildFibersInPlace( workInProgress, workInProgress.child, nextChildren, priorityLevel, ); transferDeletions(workInProgress); } markChildAsProgressed(current, workInProgress, priorityLevel); } function updateFragment(current, workInProgress) { var nextChildren = workInProgress.pendingProps; if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextChildren === null) { nextChildren = workInProgress.memoizedProps; } } else if ( nextChildren === null || workInProgress.memoizedProps === nextChildren ) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextChildren); memoizeProps(workInProgress, nextChildren); return workInProgress.child; } function markRef(current: Fiber | null, workInProgress: Fiber) { const ref = workInProgress.ref; if (ref !== null && (!current || current.ref !== ref)) { // Schedule a Ref effect workInProgress.effectTag |= Ref; } } function updateFunctionalComponent(current, workInProgress) { var fn = workInProgress.type; var nextProps = workInProgress.pendingProps; const memoizedProps = workInProgress.memoizedProps; if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextProps === null) { nextProps = memoizedProps; } } else { if (nextProps === null || memoizedProps === nextProps) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } // TODO: Disable this before release, since it is not part of the public API // I use this for testing to compare the relative overhead of classes. if ( typeof fn.shouldComponentUpdate === 'function' && !fn.shouldComponentUpdate(memoizedProps, nextProps) ) { // Memoize props even if shouldComponentUpdate returns false memoizeProps(workInProgress, nextProps); return bailoutOnAlreadyFinishedWork(current, workInProgress); } } var unmaskedContext = getUnmaskedContext(workInProgress); var context = getMaskedContext(workInProgress, unmaskedContext); var nextChildren; if (__DEV__) { ReactCurrentOwner.current = workInProgress; ReactDebugCurrentFiber.phase = 'render'; nextChildren = fn(nextProps, context); ReactDebugCurrentFiber.phase = null; } else { nextChildren = fn(nextProps, context); } reconcileChildren(current, workInProgress, nextChildren); memoizeProps(workInProgress, nextProps); return workInProgress.child; } function updateClassComponent( current: Fiber | null, workInProgress: Fiber, priorityLevel: PriorityLevel, ) { // Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. const hasContext = pushContextProvider(workInProgress); let shouldUpdate; if (current === null) { if (!workInProgress.stateNode) { // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, workInProgress.pendingProps); mountClassInstance(workInProgress, priorityLevel); shouldUpdate = true; } else { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel); } } else { shouldUpdate = updateClassInstance( current, workInProgress, priorityLevel, ); } return finishClassComponent( current, workInProgress, shouldUpdate, hasContext, ); } function finishClassComponent( current: Fiber | null, workInProgress: Fiber, shouldUpdate: boolean, hasContext: boolean, ) { // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress); if (!shouldUpdate) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } const instance = workInProgress.stateNode; // Rerender ReactCurrentOwner.current = workInProgress; let nextChildren; if (__DEV__) { ReactDebugCurrentFiber.phase = 'render'; nextChildren = instance.render(); ReactDebugCurrentFiber.phase = null; } else { nextChildren = instance.render(); } reconcileChildren(current, workInProgress, nextChildren); // Memoize props and state using the values we just used to render. // TODO: Restructure so we never read values from the instance. memoizeState(workInProgress, instance.state); memoizeProps(workInProgress, instance.props); // The context might have changed so we need to recalculate it. if (hasContext) { invalidateContextProvider(workInProgress); } return workInProgress.child; } function updateHostRoot(current, workInProgress, priorityLevel) { const root = (workInProgress.stateNode: FiberRoot); if (root.pendingContext) { pushTopLevelContextObject( workInProgress, root.pendingContext, root.pendingContext !== root.context, ); } else if (root.context) { // Should always be set pushTopLevelContextObject(workInProgress, root.context, false); } pushHostContainer(workInProgress, root.containerInfo); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { const prevState = workInProgress.memoizedState; const state = beginUpdateQueue( workInProgress, updateQueue, null, prevState, null, priorityLevel, ); if (prevState === state) { // If the state is the same as before, that's a bailout because we had // no work matching this priority. return bailoutOnAlreadyFinishedWork(current, workInProgress); } const element = state.element; reconcileChildren(current, workInProgress, element); memoizeState(workInProgress, state); return workInProgress.child; } // If there is no update queue, that's a bailout because the root has no props. return bailoutOnAlreadyFinishedWork(current, workInProgress); } function updateHostComponent(current, workInProgress) { pushHostContext(workInProgress); let nextProps = workInProgress.pendingProps; const prevProps = current !== null ? current.memoizedProps : null; const memoizedProps = workInProgress.memoizedProps; if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextProps === null) { nextProps = memoizedProps; invariant( nextProps !== null, 'We should always have pending or current props. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } else if (nextProps === null || memoizedProps === nextProps) { if ( !useSyncScheduling && shouldDeprioritizeSubtree(workInProgress.type, memoizedProps) && workInProgress.pendingWorkPriority !== OffscreenPriority ) { // This subtree still has work, but it should be deprioritized so we need // to bail out and not do any work yet. // TODO: It would be better if this tree got its correct priority set // during scheduleUpdate instead because otherwise we'll start a higher // priority reconciliation first before we can get down here. However, // that is a bit tricky since workInProgress and current can have // different "hidden" settings. let child = workInProgress.progressedChild; while (child !== null) { // To ensure that this subtree gets its priority reset, the children // need to be reset. child.pendingWorkPriority = OffscreenPriority; child = child.sibling; } return null; } return bailoutOnAlreadyFinishedWork(current, workInProgress); } let nextChildren = nextProps.children; const isDirectTextChild = shouldSetTextContent(nextProps); if (isDirectTextChild) { // We special case a direct text child of a host node. This is a common // case. We won't handle it as a reified child. We will instead handle // this in the host environment that also have access to this prop. That // avoids allocating another HostText fiber and traversing it. nextChildren = null; } else if (prevProps && shouldSetTextContent(prevProps)) { // If we're switching from a direct text child to a normal child, or to // empty, we need to schedule the text content to be reset. workInProgress.effectTag |= ContentReset; } markRef(current, workInProgress); if ( !useSyncScheduling && shouldDeprioritizeSubtree(workInProgress.type, nextProps) && workInProgress.pendingWorkPriority !== OffscreenPriority ) { // If this host component is hidden, we can bail out on the children. // We'll rerender the children later at the lower priority. // It is unfortunate that we have to do the reconciliation of these // children already since that will add them to the tree even though // they are not actually done yet. If this is a large set it is also // confusing that this takes time to do right now instead of later. if (workInProgress.progressedPriority === OffscreenPriority) { // If we already made some progress on the offscreen priority before, // then we should continue from where we left off. workInProgress.child = workInProgress.progressedChild; } // Reconcile the children and stash them for later work. reconcileChildrenAtPriority( current, workInProgress, nextChildren, OffscreenPriority, ); memoizeProps(workInProgress, nextProps); workInProgress.child = current !== null ? current.child : null; if (current === null) { // If this doesn't have a current we won't track it for placement // effects. However, when we come back around to this we have already // inserted the parent which means that we'll infact need to make this a // placement. // TODO: There has to be a better solution to this problem. let child = workInProgress.progressedChild; while (child !== null) { child.effectTag = Placement; child = child.sibling; } } // Abort and don't process children yet. return null; } else { reconcileChildren(current, workInProgress, nextChildren); memoizeProps(workInProgress, nextProps); return workInProgress.child; } } function updateHostText(current, workInProgress) { let nextProps = workInProgress.pendingProps; if (nextProps === null) { nextProps = workInProgress.memoizedProps; } memoizeProps(workInProgress, nextProps); // Nothing to do here. This is terminal. We'll do the completion step // immediately after. return null; } function mountIndeterminateComponent(current, workInProgress, priorityLevel) { invariant( current === null, 'An indeterminate component should never have mounted. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); var fn = workInProgress.type; var props = workInProgress.pendingProps; var unmaskedContext = getUnmaskedContext(workInProgress); var context = getMaskedContext(workInProgress, unmaskedContext); var value; if (__DEV__) { ReactCurrentOwner.current = workInProgress; value = fn(props, context); } else { value = fn(props, context); } if ( typeof value === 'object' && value !== null && typeof value.render === 'function' ) { // Proceed under the assumption that this is a class instance workInProgress.tag = ClassComponent; // Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. const hasContext = pushContextProvider(workInProgress); adoptClassInstance(workInProgress, value); mountClassInstance(workInProgress, priorityLevel); return finishClassComponent(current, workInProgress, true, hasContext); } else { // Proceed under the assumption that this is a functional component workInProgress.tag = FunctionalComponent; if (__DEV__) { const Component = workInProgress.type; if (Component) { warning( !Component.childContextTypes, '%s(...): childContextTypes cannot be defined on a functional component.', Component.displayName || Component.name || 'Component', ); } if (workInProgress.ref !== null) { let info = ''; const ownerName = ReactDebugCurrentFiber.getCurrentFiberOwnerName(); if (ownerName) { info += '\n\nCheck the render method of `' + ownerName + '`.'; } let warningKey = ownerName || workInProgress._debugID || ''; const debugSource = workInProgress._debugSource; if (debugSource) { warningKey = debugSource.fileName + ':' + debugSource.lineNumber; } if (!warnedAboutStatelessRefs[warningKey]) { warnedAboutStatelessRefs[warningKey] = true; warning( false, 'Stateless function components cannot be given refs. ' + 'Attempts to access this ref will fail.%s%s', info, ReactDebugCurrentFiber.getCurrentFiberStackAddendum(), ); } } } reconcileChildren(current, workInProgress, value); memoizeProps(workInProgress, props); return workInProgress.child; } } function updateCoroutineComponent(current, workInProgress) { var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine); if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextCoroutine === null) { nextCoroutine = current && current.memoizedProps; invariant( nextCoroutine !== null, 'We should always have pending or current props. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } else if ( nextCoroutine === null || workInProgress.memoizedProps === nextCoroutine ) { nextCoroutine = workInProgress.memoizedProps; // TODO: When bailing out, we might need to return the stateNode instead // of the child. To check it for work. // return bailoutOnAlreadyFinishedWork(current, workInProgress); } const nextChildren = nextCoroutine.children; const priorityLevel = workInProgress.pendingWorkPriority; // The following is a fork of reconcileChildrenAtPriority but using // stateNode to store the child. // At this point any memoization is no longer valid since we'll have changed // the children. workInProgress.memoizedProps = null; if (current === null) { workInProgress.stateNode = mountChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, priorityLevel, ); } else if (current.child === workInProgress.child) { clearDeletions(workInProgress); workInProgress.stateNode = reconcileChildFibers( workInProgress, workInProgress.stateNode, nextChildren, priorityLevel, ); transferDeletions(workInProgress); } else { workInProgress.stateNode = reconcileChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, priorityLevel, ); transferDeletions(workInProgress); } memoizeProps(workInProgress, nextCoroutine); // This doesn't take arbitrary time so we could synchronously just begin // eagerly do the work of workInProgress.child as an optimization. return workInProgress.stateNode; } function updatePortalComponent(current, workInProgress) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); const priorityLevel = workInProgress.pendingWorkPriority; let nextChildren = workInProgress.pendingProps; if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextChildren === null) { nextChildren = current && current.memoizedProps; invariant( nextChildren != null, 'We should always have pending or current props. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } else if ( nextChildren === null || workInProgress.memoizedProps === nextChildren ) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } if (current === null) { // Portals are special because we don't append the children during mount // but at commit. Therefore we need to track insertions which the normal // flow doesn't do during mount. This doesn't happen at the root because // the root always starts with a "current" with a null child. // TODO: Consider unifying this with how the root works. workInProgress.child = reconcileChildFibersInPlace( workInProgress, workInProgress.child, nextChildren, priorityLevel, ); memoizeProps(workInProgress, nextChildren); markChildAsProgressed(current, workInProgress, priorityLevel); } else { reconcileChildren(current, workInProgress, nextChildren); memoizeProps(workInProgress, nextChildren); } return workInProgress.child; } /* function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) { let child = firstChild; do { // Ensure that the first and last effect of the parent corresponds // to the children's first and last effect. if (!returnFiber.firstEffect) { returnFiber.firstEffect = child.firstEffect; } if (child.lastEffect) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = child.firstEffect; } returnFiber.lastEffect = child.lastEffect; } } while (child = child.sibling); } */ function bailoutOnAlreadyFinishedWork( current, workInProgress: Fiber, ): Fiber | null { if (__DEV__) { cancelWorkTimer(workInProgress); } const priorityLevel = workInProgress.pendingWorkPriority; // TODO: We should ideally be able to bail out early if the children have no // more work to do. However, since we don't have a separation of this // Fiber's priority and its children yet - we don't know without doing lots // of the same work we do anyway. Once we have that separation we can just // bail out here if the children has no more work at this priority level. // if (workInProgress.priorityOfChildren <= priorityLevel) { // // If there are side-effects in these children that have not yet been // // committed we need to ensure that they get properly transferred up. // if (current && current.child !== workInProgress.child) { // reuseChildrenEffects(workInProgress, child); // } // return null; // } if (current && workInProgress.child === current.child) { // If we had any progressed work already, that is invalid at this point so // let's throw it out. clearDeletions(workInProgress); } cloneChildFibers(current, workInProgress); markChildAsProgressed(current, workInProgress, priorityLevel); return workInProgress.child; } function bailoutOnLowPriority(current, workInProgress) { if (__DEV__) { cancelWorkTimer(workInProgress); } // TODO: Handle HostComponent tags here as well and call pushHostContext()? // See PR 8590 discussion for context switch (workInProgress.tag) { case ClassComponent: pushContextProvider(workInProgress); break; case HostPortal: pushHostContainer( workInProgress, workInProgress.stateNode.containerInfo, ); break; } // TODO: What if this is currently in progress? // How can that happen? How is this not being cloned? return null; } function memoizeProps(workInProgress: Fiber, nextProps: any) { workInProgress.memoizedProps = nextProps; // Reset the pending props workInProgress.pendingProps = null; } function memoizeState(workInProgress: Fiber, nextState: any) { workInProgress.memoizedState = nextState; // Don't reset the updateQueue, in case there are pending updates. Resetting // is handled by beginUpdateQueue. } function beginWork( current: Fiber | null, workInProgress: Fiber, priorityLevel: PriorityLevel, ): Fiber | null { if ( workInProgress.pendingWorkPriority === NoWork || workInProgress.pendingWorkPriority > priorityLevel ) { return bailoutOnLowPriority(current, workInProgress); } if (__DEV__) { ReactDebugCurrentFiber.current = workInProgress; } // If we don't bail out, we're going be recomputing our children so we need // to drop our effect list. workInProgress.firstEffect = null; workInProgress.lastEffect = null; if (workInProgress.progressedPriority === priorityLevel) { // If we have progressed work on this priority level already, we can // proceed this that as the child. workInProgress.child = workInProgress.progressedChild; } switch (workInProgress.tag) { case IndeterminateComponent: return mountIndeterminateComponent( current, workInProgress, priorityLevel, ); case FunctionalComponent: return updateFunctionalComponent(current, workInProgress); case ClassComponent: return updateClassComponent(current, workInProgress, priorityLevel); case HostRoot: return updateHostRoot(current, workInProgress, priorityLevel); case HostComponent: return updateHostComponent(current, workInProgress); case HostText: return updateHostText(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: return updateCoroutineComponent(current, workInProgress); case YieldComponent: // A yield component is just a placeholder, we can just run through the // next one immediately. return null; case HostPortal: return updatePortalComponent(current, workInProgress); case Fragment: return updateFragment(current, workInProgress); default: invariant( false, 'Unknown unit of work tag. This error is likely caused by a bug in ' + 'React. Please file an issue.', ); } } function beginFailedWork( current: Fiber | null, workInProgress: Fiber, priorityLevel: PriorityLevel, ) { invariant( workInProgress.tag === ClassComponent || workInProgress.tag === HostRoot, 'Invalid type of work. This error is likely caused by a bug in React. ' + 'Please file an issue.', ); // Add an error effect so we can handle the error during the commit phase workInProgress.effectTag |= Err; if ( workInProgress.pendingWorkPriority === NoWork || workInProgress.pendingWorkPriority > priorityLevel ) { return bailoutOnLowPriority(current, workInProgress); } // If we don't bail out, we're going be recomputing our children so we need // to drop our effect list. workInProgress.firstEffect = null; workInProgress.lastEffect = null; // Unmount the current children as if the component rendered null const nextChildren = null; reconcileChildren(current, workInProgress, nextChildren); if (workInProgress.tag === ClassComponent) { const instance = workInProgress.stateNode; workInProgress.memoizedProps = instance.props; workInProgress.memoizedState = instance.state; workInProgress.pendingProps = null; } return workInProgress.child; } return { beginWork, beginFailedWork, }; };