UNPKG

react-native

Version:

A framework for building native apps using React

796 lines (711 loc) • 29.8 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 ReactCurrentOwner = require('react/lib/ReactCurrentOwner'); var ReactFiberClassComponent = require('ReactFiberClassComponent'); var invariant = require('fbjs/lib/invariant'); if (__DEV__) { var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); 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 : () => PriorityLevel, ) { const { shouldSetTextContent } = 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; nextChildren = fn(nextProps, context); } 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); 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; const 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 (memoizedProps.hidden && 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 (nextProps.hidden && 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 { 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) { // 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, }; };