UNPKG

react-dom

Version:

React package for working with the DOM.

612 lines (538 loc) • 26.1 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. * * */ 'use strict'; var _require = require('./ReactChildFiber'), mountChildFibersInPlace = _require.mountChildFibersInPlace, reconcileChildFibers = _require.reconcileChildFibers, reconcileChildFibersInPlace = _require.reconcileChildFibersInPlace, cloneChildFibers = _require.cloneChildFibers; var _require2 = require('./ReactFiberUpdateQueue'), beginUpdateQueue = _require2.beginUpdateQueue; var ReactTypeOfWork = require('./ReactTypeOfWork'); var _require3 = require('./ReactFiberContext'), getMaskedContext = _require3.getMaskedContext, getUnmaskedContext = _require3.getUnmaskedContext, hasContextChanged = _require3.hasContextChanged, pushContextProvider = _require3.pushContextProvider, pushTopLevelContextObject = _require3.pushTopLevelContextObject, invalidateContextProvider = _require3.invalidateContextProvider; var IndeterminateComponent = ReactTypeOfWork.IndeterminateComponent, FunctionalComponent = ReactTypeOfWork.FunctionalComponent, ClassComponent = ReactTypeOfWork.ClassComponent, HostRoot = ReactTypeOfWork.HostRoot, HostComponent = ReactTypeOfWork.HostComponent, HostText = ReactTypeOfWork.HostText, HostPortal = ReactTypeOfWork.HostPortal, CoroutineComponent = ReactTypeOfWork.CoroutineComponent, CoroutineHandlerPhase = ReactTypeOfWork.CoroutineHandlerPhase, YieldComponent = ReactTypeOfWork.YieldComponent, Fragment = ReactTypeOfWork.Fragment; var _require4 = require('./ReactPriorityLevel'), NoWork = _require4.NoWork, OffscreenPriority = _require4.OffscreenPriority; var _require5 = require('./ReactTypeOfSideEffect'), Update = _require5.Update, Placement = _require5.Placement, ContentReset = _require5.ContentReset, Err = _require5.Err, Ref = _require5.Ref; var ReactCurrentOwner = require('react/lib/ReactCurrentOwner'); var ReactFiberClassComponent = require('./ReactFiberClassComponent'); var warning = require('fbjs/lib/warning'); if (process.env.NODE_ENV !== 'production') { var ReactDebugCurrentFiber = require('./ReactDebugCurrentFiber'); } module.exports = function (config, hostContext, scheduleUpdate, getPriorityContext) { var shouldSetTextContent = config.shouldSetTextContent; var pushHostContext = hostContext.pushHostContext, pushHostContainer = hostContext.pushHostContainer; var _ReactFiberClassCompo = ReactFiberClassComponent(scheduleUpdate, getPriorityContext), adoptClassInstance = _ReactFiberClassCompo.adoptClassInstance, constructClassInstance = _ReactFiberClassCompo.constructClassInstance, mountClassInstance = _ReactFiberClassCompo.mountClassInstance, resumeMountClassInstance = _ReactFiberClassCompo.resumeMountClassInstance, updateClassInstance = _ReactFiberClassCompo.updateClassInstance; 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) { // 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) { var 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) { // 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 = current && current.memoizedProps; } } else if (nextChildren === null || workInProgress.memoizedProps === nextChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; } function markRef(current, workInProgress) { var ref = workInProgress.ref; if (ref && (!current || current.ref !== ref)) { // Schedule a Ref effect workInProgress.effectTag |= Ref; } } function updateFunctionalComponent(current, workInProgress) { var fn = workInProgress.type; var nextProps = workInProgress.pendingProps; var 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 = current && current.memoizedProps; } } else if (nextProps === null || memoizedProps === nextProps || // 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. memoizedProps !== null && typeof fn.shouldComponentUpdate === 'function' && !fn.shouldComponentUpdate(memoizedProps, nextProps)) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } var unmaskedContext = getUnmaskedContext(workInProgress); var context = getMaskedContext(workInProgress, unmaskedContext); var nextChildren; if (process.env.NODE_ENV !== 'production') { ReactCurrentOwner.current = workInProgress; nextChildren = fn(nextProps, context); } else { nextChildren = fn(nextProps, context); } reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; } function updateClassComponent(current, workInProgress, 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. var hasContext = pushContextProvider(workInProgress); var shouldUpdate = void 0; if (!current) { 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, workInProgress, shouldUpdate, hasContext) { // Schedule side-effects // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress); if (shouldUpdate) { workInProgress.effectTag |= Update; } else { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. if (current) { var _instance = current.stateNode; if (_instance.props !== current.memoizedProps || _instance.state !== current.memoizedState) { workInProgress.effectTag |= Update; } } return bailoutOnAlreadyFinishedWork(current, workInProgress); } // Rerender var instance = workInProgress.stateNode; ReactCurrentOwner.current = workInProgress; var nextChildren = instance.render(); reconcileChildren(current, workInProgress, nextChildren); // The context might have changed so we need to recalculate it. if (hasContext) { invalidateContextProvider(workInProgress); } return workInProgress.child; } function updateHostRoot(current, workInProgress, priorityLevel) { var root = workInProgress.stateNode; 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); var updateQueue = workInProgress.updateQueue; if (updateQueue) { var prevState = workInProgress.memoizedState; var 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); } var element = state.element; reconcileChildren(current, workInProgress, element); workInProgress.memoizedState = 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); var nextProps = workInProgress.pendingProps; var prevProps = current ? current.memoizedProps : null; var 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 = prevProps; if (!nextProps) { throw new Error('We should always have pending or current props.'); } } } 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. var child = workInProgress.progressedChild; while (child) { // 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); } var nextChildren = nextProps.children; var 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); workInProgress.child = current ? current.child : null; if (!current) { // 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. var _child = workInProgress.progressedChild; while (_child) { _child.effectTag = Placement; _child = _child.sibling; } } // Abort and don't process children yet. return null; } else { reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; } } function mountIndeterminateComponent(current, workInProgress, priorityLevel) { if (current) { throw new Error('An indeterminate component should never have mounted.'); } var fn = workInProgress.type; var props = workInProgress.pendingProps; var unmaskedContext = getUnmaskedContext(workInProgress); var context = getMaskedContext(workInProgress, unmaskedContext); var value; if (process.env.NODE_ENV !== 'production') { ReactCurrentOwner.current = workInProgress; value = fn(props, context); } else { value = fn(props, context); } if (typeof value === 'object' && value && 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. var 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 (process.env.NODE_ENV !== 'production') { if (workInProgress.ref != null) { var info = ''; var ownerName = ReactDebugCurrentFiber.getCurrentFiberOwnerName(); if (ownerName) { info += ' Check the render method of `' + ownerName + '`.'; } process.env.NODE_ENV !== 'production' ? warning(false, 'Stateless function components cannot be given refs. ' + 'Attempts to access this ref will fail.%s%s', info, ReactDebugCurrentFiber.getCurrentFiberStackAddendum()) : void 0; } } reconcileChildren(current, workInProgress, value); return workInProgress.child; } } function updateCoroutineComponent(current, workInProgress) { var nextCoroutine = 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 (nextCoroutine === null) { nextCoroutine = current && current.memoizedProps; if (!nextCoroutine) { throw new Error('We should always have pending or current props.'); } } } else if (nextCoroutine === null || workInProgress.memoizedProps === nextCoroutine) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextCoroutine.children); // This doesn't take arbitrary time so we could synchronously just begin // eagerly do the work of workInProgress.child as an optimization. return workInProgress.child; } function updatePortalComponent(current, workInProgress) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); var priorityLevel = workInProgress.pendingWorkPriority; 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 = current && current.memoizedProps; if (!nextChildren) { throw new Error('We should always have pending or current props.'); } } } else if (nextChildren === null || workInProgress.memoizedProps === nextChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } if (!current) { // 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); markChildAsProgressed(current, workInProgress, priorityLevel); } else { reconcileChildren(current, 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) { var 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 beginWork(current, workInProgress, priorityLevel) { if (workInProgress.pendingWorkPriority === NoWork || workInProgress.pendingWorkPriority > priorityLevel) { return bailoutOnLowPriority(current, workInProgress); } if (process.env.NODE_ENV !== 'production') { 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: // Nothing to do here. This is terminal. We'll do the completion step // immediately after. return null; 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: throw new Error('Unknown unit of work tag'); } } function beginFailedWork(current, workInProgress, priorityLevel) { if (workInProgress.tag !== ClassComponent && workInProgress.tag !== HostRoot) { throw new Error('Invalid type of work'); } // 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 var nextChildren = null; reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; } return { beginWork: beginWork, beginFailedWork: beginFailedWork }; };