koba04 / react-fiber-resources

Resources for React Fiber
MIT License
783 stars 44 forks source link

Trace to render ReactDOMFiber #1

Closed koba04 closed 7 years ago

koba04 commented 8 years ago

This issue is for to trace ReactDOMFiber rendering.

const Foo = ({child}) => <div>{child}</div>;
ReactDOMFiber.render(
  <Foo child="test" />,
  document.createElement('div')
);

🚀 🚀

This is based on v15.4.0-rc.3

koba04 commented 8 years ago
  render(element : ReactElement<any>, container : DOMContainerElement) {
    warnAboutUnstableUse();
    if (!container._reactRootContainer) {
      container._reactRootContainer = DOMRenderer.mountContainer(element, container);
    } else {
      DOMRenderer.updateContainer(element, container._reactRootContainer);
    }
  },

In mounting phase, render uses DOMRenderer.mountContainer for mounting a component.

koba04 commented 8 years ago
var DOMRenderer = ReactFiberReconciler({

  updateContainer(container : Container, children : HostChildren<Instance>) : void {
    container.innerHTML = '';
    recursivelyAppendChildren(container, children);
  },

  createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
    const domElement = document.createElement(type);
    recursivelyAppendChildren(domElement, children);
    if (typeof props.children === 'string') {
      domElement.textContent = props.children;
    }
    return domElement;
  },

  prepareUpdate(
    domElement : Instance,
    oldProps : Props,
    newProps : Props,
    children : HostChildren<Instance>
  ) : boolean {
    return true;
  },

  commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
    domElement.innerHTML = '';
    recursivelyAppendChildren(domElement, children);
    if (typeof newProps.children === 'string') {
      domElement.textContent = newProps.children;
    }
  },

  deleteInstance(instance : Instance) : void {
    // Noop
  },

  scheduleAnimationCallback: window.requestAnimationFrame,

  scheduleDeferredCallback: window.requestIdleCallback,

});

DOMRenderer is created by ReactFiberReconciler, which is passed configs such as updateContainer, createInstance, prepareUpdate, commitUpdate, deleteInstance, scheduleAnimationCallback and scheduleDeferredCallback.

You can see scheduleAnimationCallback equals to window.requestAnimationFrame, scheduleDeferredCallback equals to window.requestIdleCallback.

koba04 commented 8 years ago
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconciler<C> {

  var { scheduleWork, performWithPriority } = ReactFiberScheduler(config);

  return {

    mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode {
      const root = createFiberRoot(containerInfo);
      const container = root.current;
      // TODO: Use pending work/state instead of props.
      // TODO: This should not override the pendingWorkPriority if there is
      // higher priority work in the subtree.
      container.pendingProps = element;

      scheduleWork(root);

      // It may seem strange that we don't return the root here, but that will
      // allow us to have containers that are in the middle of the tree instead
      // of being roots.
      return container;
    },

    updateContainer(element : ReactElement<any>, container : OpaqueNode) : void {
      // TODO: If this is a nested container, this won't be the root.
      const root : FiberRoot = (container.stateNode : any);
      // TODO: Use pending work/state instead of props.
      root.current.pendingProps = element;

      scheduleWork(root);
    },

    unmountContainer(container : OpaqueNode) : void {
      // TODO: If this is a nested container, this won't be the root.
      const root : FiberRoot = (container.stateNode : any);
      // TODO: Use pending work/state instead of props.
      root.current.pendingProps = [];

      scheduleWork(root);
    },

    performWithPriority,

    getPublicRootInstance(container : OpaqueNode) : (C | null) {
      return null;
    },

  };

};

ReactFiberReconciler returns an object, which has mountContainer, updateContainer, unmountContainer, performWithPriority and getPublicRootInstance methods.

ReactDOMFiber calls mountContainer. Let's get into the mountContainer 🚀

koba04 commented 8 years ago

mountContainer calls createFiberRoot with containerInfo. In this case, containerInfo is a DOM Element mounting a ReactElement.

exports.createFiberRoot = function(containerInfo : any) : FiberRoot {
  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  const uninitializedFiber = createHostContainerFiber();
  const root = {
    current: uninitializedFiber,
    containerInfo: containerInfo,
    isScheduled: false,
    nextScheduledRoot: null,
  };
  uninitializedFiber.stateNode = root;
  return root;
};

createFiberRoot creates FiberRoot with HostContainerFiber. createHostContainerFiber is

exports.createHostContainerFiber = function() {
  const fiber = createFiber(HostContainer, null);
  return fiber;
};

createHostContainerFiber creates a Fiber for HostContainer.

HostContainer is a Fiber for a root of a host tree.

module.exports = {
  IndeterminateComponent: 0, // Before we know whether it is functional or class
  FunctionalComponent: 1,
  ClassComponent: 2,
  HostContainer: 3, // Root of a host tree. Could be nested inside another node.
  HostComponent: 4,
  CoroutineComponent: 5,
  CoroutineHandlerPhase: 6,
  YieldComponent: 7,
};

createFiber creates a Fiber.

var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
  return {

    // Instance

    tag: tag,

    key: key,

    type: null,

    stateNode: null,

    // Fiber

    return: null,

    child: null,
    sibling: null,

    ref: null,

    pendingProps: null,
    memoizedProps: null,
    updateQueue: null,
    memoizedState: null,
    callbackList: null,
    output: null,

    nextEffect: null,
    firstEffect: null,
    lastEffect: null,

    pendingWorkPriority: NoWork,
    progressedPriority: NoWork,
    progressedChild: null,

    alternate: null,

  };
};

As the result, rootFiber is like this

rootFiber = {
  current: hostContainerFiber
  containerInfo: rootOfHostTree,
  isScheduled: false,
  nextScheduledRoot: null,
};
// circular reference (what is this for?)
rootFiber.current.stateNode = rootFiber;
koba04 commented 8 years ago

go back mountContainer, now we've got fiberRoot, a magic of ReactFiber seems to be in scheduleWork(root)

      const root = createFiberRoot(containerInfo);
      const container = root.current;
      // TODO: Use pending work/state instead of props.
      // TODO: This should not override the pendingWorkPriority if there is
      // higher priority work in the subtree.
      container.pendingProps = element;

      scheduleWork(root);

      // It may seem strange that we don't return the root here, but that will
      // allow us to have containers that are in the middle of the tree instead
      // of being roots.
      return container;

Before scheduleWork, rootFiber is like this:

``` js // an output from console.log(root) Object { current: Object { tag: 3, key: null, type: null, stateNode: [Circular], return: null, child: null, sibling: null, ref: null, pendingProps: Object { '$$typeof': Symbol(react.element), type: [Function], key: null, ref: null, props: [Object], _owner: null, _store: Object {} }, memoizedProps: null, updateQueue: null, memoizedState: null, callbackList: null, output: null, nextEffect: null, firstEffect: null, lastEffect: null, pendingWorkPriority: 0, progressedPriority: 0, progressedChild: null, alternate: null }, containerInfo: HTMLDivElement {}, isScheduled: false, nextScheduledRoot: null } ```

After scheduleWork, rootFiber is like this:

``` js // an output from console.log(root) Object { current: Object { tag: 3, key: null, type: null, stateNode: [Circular], return: null, child: Object { tag: 1, key: null, type: [Function], stateNode: null, return: [Circular], child: [Object], sibling: null, ref: null, pendingProps: null, memoizedProps: [Object], updateQueue: null, memoizedState: null, callbackList: null, output: HTMLDivElement {}, nextEffect: null, firstEffect: null, lastEffect: null, pendingWorkPriority: 0, progressedPriority: 4, progressedChild: [Object], alternate: null }, sibling: null, ref: null, pendingProps: null, memoizedProps: Object { '$$typeof': Symbol(react.element), type: [Function], key: null, ref: null, props: [Object], _owner: null, _store: Object {} }, updateQueue: null, memoizedState: null, callbackList: null, output: HTMLDivElement {}, nextEffect: null, firstEffect: [Circular], lastEffect: [Circular], pendingWorkPriority: 0, progressedPriority: 4, progressedChild: Object { tag: 1, key: null, type: [Function], stateNode: null, return: [Circular], child: [Object], sibling: null, ref: null, pendingProps: null, memoizedProps: [Object], updateQueue: null, memoizedState: null, callbackList: null, output: HTMLDivElement {}, nextEffect: null, firstEffect: null, lastEffect: null, pendingWorkPriority: 0, progressedPriority: 4, progressedChild: [Object], alternate: null }, alternate: Object { tag: 3, key: null, type: null, stateNode: [Circular], return: null, child: null, sibling: null, ref: null, pendingProps: [Object], memoizedProps: null, updateQueue: null, memoizedState: null, callbackList: null, output: null, nextEffect: null, firstEffect: null, lastEffect: null, pendingWorkPriority: 4, progressedPriority: 4, progressedChild: [Object], alternate: [Circular] } }, containerInfo: HTMLDivElement {}, isScheduled: false, nextScheduledRoot: null } ```
koba04 commented 8 years ago

scheduleWork is in ReactFiberScheduler.

  function scheduleWork(root : FiberRoot) {
    if (defaultPriority === SynchronousPriority) {
      throw new Error('Not implemented yet');
    }

    if (defaultPriority === NoWork) {
      return;
    }
    if (defaultPriority > AnimationPriority) {
      scheduleDeferredWork(root, defaultPriority);
      return;
    }
    scheduleAnimationWork(root, defaultPriority);
  }

In this case, defaultPriority is LowPriority

What is Priority?

module.exports = {
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  AnimationPriority: 2, // Needs to complete before the next frame.
  HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4, // Data fetching, or result from updating stores.
  OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};

So scheduleWork calls scheduleDeferredWork because LowPriority is greater than AnimationPriority.

koba04 commented 8 years ago

scheduleDeferredWork is like this

  function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) {
    // We must reset the current unit of work pointer so that we restart the
    // search from the root during the next tick, in case there is now higher
    // priority work somewhere earlier than before.
    if (priority <= nextPriorityLevel) {
      nextUnitOfWork = null;
    }

    // Set the priority on the root, without deprioritizing
    if (root.current.pendingWorkPriority === NoWork ||
        priority <= root.current.pendingWorkPriority) {
      root.current.pendingWorkPriority = priority;
    }

    if (root.isScheduled) {
      // If we're already scheduled, we can bail out.
      return;
    }
    root.isScheduled = true;
    if (lastScheduledRoot) {
      // Schedule ourselves to the end.
      lastScheduledRoot.nextScheduledRoot = root;
      lastScheduledRoot = root;
    } else {
      // We're the only work scheduled.
      nextScheduledRoot = root;
      lastScheduledRoot = root;
      scheduleDeferredCallback(performDeferredWork);
    }
  }

priority is LowPriority, nextPriorityLevel is NoWork, root.current.pendingWorkPriority is 0, then root.current.pendingWorkPriority becomes LowPriority.

root.isScheduled becomes true and lastScheduledRoot is false, So nextScheduledRoot and lastScheduledRoot become rootFiber and calls scheduleDeferredCallback with performDeferredWork.

koba04 commented 8 years ago
  function performDeferredWork(deadline) {
    if (!nextUnitOfWork) {
      nextUnitOfWork = findNextUnitOfWork();
    }
    while (nextUnitOfWork) {
      if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        if (!nextUnitOfWork) {
          // Find more work. We might have time to complete some more.
          nextUnitOfWork = findNextUnitOfWork();
        }
      } else {
        scheduleDeferredCallback(performDeferredWork);
        return;
      }
    }
  }

nextUnitOfWork is null, so findNextUnitOfWork is called to get a nextUnitOfWork. before while loop, let's get into findNextUnitOfWork.

koba04 commented 8 years ago
  function findNextUnitOfWork() {
    // Clear out roots with no more work on them.
    while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
      nextScheduledRoot.isScheduled = false;
      if (nextScheduledRoot === lastScheduledRoot) {
        nextScheduledRoot = null;
        lastScheduledRoot = null;
        nextPriorityLevel = NoWork;
        return null;
      }
      nextScheduledRoot = nextScheduledRoot.nextScheduledRoot;
    }
    // TODO: This is scanning one root at a time. It should be scanning all
    // roots for high priority work before moving on to lower priorities.
    let root = nextScheduledRoot;
    let highestPriorityRoot = null;
    let highestPriorityLevel = NoWork;
    while (root) {
      if (highestPriorityLevel === NoWork ||
          highestPriorityLevel > root.current.pendingWorkPriority) {
        highestPriorityLevel = root.current.pendingWorkPriority;
        highestPriorityRoot = root;
      }
      // We didn't find anything to do in this root, so let's try the next one.
      root = root.nextScheduledRoot;
    }
    if (highestPriorityRoot) {
      nextPriorityLevel = highestPriorityLevel;
      return cloneFiber(
        highestPriorityRoot.current,
        highestPriorityLevel
      );
    }

    nextPriorityLevel = NoWork;
    return null;
  }

findNextUnitOfWork returns nextUnitOfWork, which is a Fiber ReactFiber should work on. In this case, it returns a cloned Fiber from nextScheduledRoot.current.

koba04 commented 8 years ago

the returned Fiber is passed to performUnitOfWork, which is to process the Fiber.

  function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
    // The current, flushed, state of this fiber is the alternate.
    // Ideally nothing should rely on this, but relying on it here
    // means that we don't need an additional field on the work in
    // progress.
    const current = workInProgress.alternate;
    const next = beginWork(current, workInProgress, nextPriorityLevel);

    if (next) {
      // If this spawns new work, do that next.
      return next;
    } else {
      // Otherwise, complete the current work.
      return completeUnitOfWork(workInProgress);
    }
  }

performUnitOfWork calls beginWork. If Fiber hasn't a next work, it calls completeUnitOfWork to complete the current work.

koba04 commented 8 years ago

beginWork is in ReactFiberBeginWork.js.

  function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
    if (workInProgress.pendingWorkPriority === NoWork ||
        workInProgress.pendingWorkPriority > priorityLevel) {
      return bailoutOnLowPriority(current, workInProgress);
    }

    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;
    }

    if ((workInProgress.pendingProps === null || (
      workInProgress.memoizedProps !== null &&
      workInProgress.pendingProps === workInProgress.memoizedProps
      )) &&
      workInProgress.updateQueue === null) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress);
    }

    switch (workInProgress.tag) {
      case IndeterminateComponent:
        return mountIndeterminateComponent(current, workInProgress);
      case FunctionalComponent:
        return updateFunctionalComponent(current, workInProgress);
      case ClassComponent:
        return updateClassComponent(current, workInProgress);
      case HostContainer:
        reconcileChildren(current, workInProgress, workInProgress.pendingProps);
        // A yield component is just a placeholder, we can just run through the
        // next one immediately.
        if (workInProgress.child) {
          return beginWork(
            workInProgress.child.alternate,
            workInProgress.child,
            priorityLevel
          );
        }
        return null;
      case HostComponent:
        if (workInProgress.stateNode && config.beginUpdate) {
          config.beginUpdate(workInProgress.stateNode);
        }
        return updateHostComponent(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:
        updateCoroutineComponent(current, workInProgress);
        // This doesn't take arbitrary time so we could synchronously just begin
        // eagerly do the work of workInProgress.child as an optimization.
        if (workInProgress.child) {
          return beginWork(
            workInProgress.child.alternate,
            workInProgress.child,
            priorityLevel
          );
        }
        return workInProgress.child;
      case YieldComponent:
        // A yield component is just a placeholder, we can just run through the
        // next one immediately.
        if (workInProgress.sibling) {
          return beginWork(
            workInProgress.sibling.alternate,
            workInProgress.sibling,
            priorityLevel
          );
        }
        return null;
      default:
        throw new Error('Unknown unit of work tag');
    }
  }
koba04 commented 8 years ago
  function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
    while (true) {
      // The current, flushed, state of this fiber is the alternate.
      // Ideally nothing should rely on this, but relying on it here
      // means that we don't need an additional field on the work in
      // progress.
      const current = workInProgress.alternate;
      const next = completeWork(current, workInProgress);

      resetWorkPriority(workInProgress);

      // The work is now done. We don't need this anymore. This flags
      // to the system not to redo any work here.
      workInProgress.pendingProps = null;
      workInProgress.updateQueue = null;

      const returnFiber = workInProgress.return;

      if (returnFiber) {
        // Ensure that the first and last effect of the parent corresponds
        // to the children's first and last effect. This probably relies on
        // children completing in order.
        if (!returnFiber.firstEffect) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }
        if (workInProgress.lastEffect) {
          if (returnFiber.lastEffect) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }
          returnFiber.lastEffect = workInProgress.lastEffect;
        }
      }

      if (next) {
        // If completing this work spawned new work, do that next.
        return next;
      } else if (workInProgress.sibling) {
        // If there is more work to do in this returnFiber, do that next.
        return workInProgress.sibling;
      } else if (returnFiber) {
        // If there's no more work in this returnFiber. Complete the returnFiber.
        workInProgress = returnFiber;
        continue;
      } else {
        // If we're at the root, there's no more work to do. We can flush it.
        const root : FiberRoot = (workInProgress.stateNode : any);
        if (root.current === workInProgress) {
          throw new Error(
            'Cannot commit the same tree as before. This is probably a bug ' +
            'related to the return field.'
          );
        }
        root.current = workInProgress;
        // TODO: We can be smarter here and only look for more work in the
        // "next" scheduled work since we've already scanned passed. That
        // also ensures that work scheduled during reconciliation gets deferred.
        // const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
        commitAllWork(workInProgress);
        const nextWork = findNextUnitOfWork();
        // if (!nextWork && hasMoreWork) {
          // TODO: This can happen when some deep work completes and we don't
          // know if this was the last one. We should be able to keep track of
          // the highest priority still in the tree for one pass. But if we
          // terminate an update we don't know.
          // throw new Error('FiberRoots should not have flagged more work if there is none.');
        // }
        return nextWork;
      }
    }
  }
koba04 commented 8 years ago
  function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
    switch (finishedWork.tag) {
      case ClassComponent: {
        // Clear updates from current fiber. This must go before the callbacks
        // are reset, in case an update is triggered from inside a callback. Is
        // this safe? Relies on the assumption that work is only committed if
        // the update queue is empty.
        if (finishedWork.alternate) {
          finishedWork.alternate.updateQueue = null;
        }
        if (finishedWork.callbackList) {
          const { callbackList } = finishedWork;
          finishedWork.callbackList = null;
          callCallbacks(callbackList, finishedWork.stateNode);
        }
        // TODO: Fire componentDidMount/componentDidUpdate, update refs
        return;
      }
      case HostContainer: {
        // TODO: Attach children to root container.
        const children = finishedWork.output;
        const root : FiberRoot = finishedWork.stateNode;
        const containerInfo : C = root.containerInfo;
        updateContainer(containerInfo, children);
        return;
      }
      case HostComponent: {
        if (finishedWork.stateNode == null || !current) {
          throw new Error('This should only be done during updates.');
        }
        // Commit the work prepared earlier.
        const child = finishedWork.child;
        const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
        const newProps = finishedWork.memoizedProps;
        const oldProps = current.memoizedProps;
        const instance : I = finishedWork.stateNode;
        commitUpdate(instance, oldProps, newProps, children);
        return;
      }
      default:
        throw new Error('This unit of work tag should not have side-effects.');
    }
  }
koba04 commented 8 years ago
  function commitAllWork(finishedWork : Fiber) {
    // Commit all the side-effects within a tree.
    // TODO: Error handling.
    let effectfulFiber = finishedWork.firstEffect;
    while (effectfulFiber) {
      const current = effectfulFiber.alternate;
      commitWork(current, effectfulFiber);
      const next = effectfulFiber.nextEffect;
      // Ensure that we clean these up so that we don't accidentally keep them.
      // I'm not actually sure this matters because we can't reset firstEffect
      // and lastEffect since they're on every node, not just the effectful
      // ones. So we have to clean everything as we reuse nodes anyway.
      effectfulFiber.nextEffect = null;
      effectfulFiber = next;
    }
  }
koba04 commented 8 years ago
  function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
    switch (finishedWork.tag) {
      case ClassComponent: {
        // Clear updates from current fiber. This must go before the callbacks
        // are reset, in case an update is triggered from inside a callback. Is
        // this safe? Relies on the assumption that work is only committed if
        // the update queue is empty.
        if (finishedWork.alternate) {
          finishedWork.alternate.updateQueue = null;
        }
        if (finishedWork.callbackList) {
          const { callbackList } = finishedWork;
          finishedWork.callbackList = null;
          callCallbacks(callbackList, finishedWork.stateNode);
        }
        // TODO: Fire componentDidMount/componentDidUpdate, update refs
        return;
      }
      case HostContainer: {
        // TODO: Attach children to root container.
        const children = finishedWork.output;
        const root : FiberRoot = finishedWork.stateNode;
        const containerInfo : C = root.containerInfo;
        updateContainer(containerInfo, children);
        return;
      }
      case HostComponent: {
        if (finishedWork.stateNode == null || !current) {
          throw new Error('This should only be done during updates.');
        }
        // Commit the work prepared earlier.
        const child = finishedWork.child;
        const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
        const newProps = finishedWork.memoizedProps;
        const oldProps = current.memoizedProps;
        const instance : I = finishedWork.stateNode;
        commitUpdate(instance, oldProps, newProps, children);
        return;
      }
      default:
        throw new Error('This unit of work tag should not have side-effects.');
    }
  }