Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Support Fiber#475

Merged
gaearon merged 21 commits intomasterfrom
fiber-3
Jan 27, 2017
Merged

Support Fiber#475
gaearon merged 21 commits intomasterfrom
fiber-3

Conversation

@gaearon
Copy link
Copy Markdown
Contributor

@gaearon gaearon commented Jan 17, 2017

This works on top of facebook/react#8818.
We diff trees on every commit and create a list of events to send to the hook.

@gaearon gaearon changed the title [WIP] Initial support for Fiber Initial (incomplete) support for Fiber Jan 17, 2017
React Fiber will check this flag before attempting to inject itself.
This avoids crashes on pre-Fiber DevTools version when Fiber is on the page.
Comment thread backend/attachRendererFiber.js Outdated
const current = root.current;
const alternate = current.alternate;
const events = [];
if (alternate && subscription != null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you're attaching to an existing tree that already has an alternate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then subscription would be null (since we haven't finished subscribing, and the React side calls the commit hook synchronously during subscription the first time).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When React calls it the first time (synchronously), then there can already be an alternate though, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. But it's && so we would go into the other branch which always mounts.

Comment thread backend/attachRendererFiber.js Outdated
const prevChild = prevChildren.get(key);
const nextChild = nextChildren.get(key);
if (prevChild != null && nextChild != null) {
// TODO: are there more cases when we can bail out?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, either all children are cloned or none are cloned. So you really only need to check nextFiber.child === prevFiber.child to know if all are the same or all are different.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an implementation detail that will hold true? I thought we might change this in the future as optimization but wasn't sure.

Comment thread backend/attachRendererFiber.js Outdated
}
}

function enqueueUpdate(fiber, events) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If props, state and the others have reference equality, you shouldn't need to push any events here. Helpful for deep updates since nothing along the path will have been updated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still need to compare children though, don't I? So that would be reference equality for everything except child ID array (which I'd compare shallowly).

Comment thread backend/attachRendererFiber.js Outdated
function updateFiber(nextFiber, prevFiber, events) {
// TODO: optimize for the common case of children with implcit keys.
// Just like we do in the Fiber child reconciler.
// We shouldn't be allocating Maps all the time.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't even need to diff on keys and types (which is incomplete anyway) since you can match on object equality between the fiber alternates.

If the first child is equal, then all the children are equal. If the first child is non-equal, you know that you will need to enqueue and update on everything that has an alternate and a mount on anything without an alternate.

The only thing you might need diffing for is unmounts. One solution is to just loop through all the next children with alternates. Add all the alternates to a Set. Then loop through all the previous children, if they're not in the Set, they're deleted.

A better solution might be to just expose a hook for deletions instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for explaining!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't even need to diff on keys and types (which is incomplete anyway)

Why is it incomplete though? What did I miss?

Comment thread backend/getDataFiber.js Outdated
};
}

module.exports = getFiberData;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: File name is getDataFiber; exports function getFiberData. The latter sounds less awkward.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing with attachFiberRenderer.

Copy link
Copy Markdown
Contributor Author

@gaearon gaearon Jan 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have existing getData013, getData012. I was striving for them all to appear together with their non-Fiber equivalents. But I don't care, can change for readability too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Didn't notice the inconsistency—probably because I changed it some point. Which way do you prefer?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd maybe vote for consistency w/r/t getData12? so getDataFiber

@gaearon gaearon changed the title Initial (incomplete) support for Fiber Support Fiber Jan 26, 2017
@gaearon
Copy link
Copy Markdown
Contributor Author

gaearon commented Jan 26, 2017

Ready for another review.

Copy link
Copy Markdown
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts! 😄 Probably should be reviewed by at least one more person though.

Comment thread backend/attachRendererFiber.js Outdated
element: getOpaqueNode(fiber),
data: getDataFiber(fiber, getOpaqueNode),
renderer: rid,
_event: 'mount',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why not type or _type (eg event.type seems more intuitive than event._event)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly I wasn't sure if any downstream code handling these events gives any meaning to event.type or other property names. I'll check and rename. 😄

Comment thread backend/attachRendererFiber.js Outdated
}
opaqueNodes.delete(fiber);
if (fiber.alternate != null) {
opaqueNodes.delete(fiber.alternate);
Copy link
Copy Markdown
Contributor

@bvaughn bvaughn Jan 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If you stored the value value returned on line 127 you would only have to delete that (and could avoid checking fiber.alternate again).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

let node = fiber;
outer: while (true) {
if (node.child) {
node.child.return = node;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this function modify return pointers? That doesn't seem right. Am I reading this wrong? (Also when would node.child.return ever not equal node?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In React source, we do this in all traversals to avoid inifinite loops. They happen if return points to alternate of the parent you're traversing.

I'm not sure if it's necessary here but I did it for extra safety. It should be fine but maybe @spicyj or @acdlite can verify.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They happen if return points to alternate of the parent you're traversing.

Wouldn't this only happen if there was a bug in fiber?

I expect these return assignments in devtools are basically neutral since the pointers should be correct to begin with, but it still seems weird to see something outside of React setting values on fibers.

Copy link
Copy Markdown
Contributor Author

@gaearon gaearon Jan 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this only happen if there was a bug in fiber?

I don't know if Fiber guarantees that returns will match up anywhere. Fiber itself always reassigns them when traversing so that's what I did. I understand it seems a bit fishy but I don't see any harm in this.

There should be no observable side effects to reassigning return I think. However, if return doesn't match, we risk getting an infinite loop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't be too bad to use recursion here instead if you wanted to avoid the concern and the implementation details of return. But then again, maybe this could should live colocated with the React Fiber source code so it feels less smelly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meh, we rely on implementation details a lot anyway. I think it's fine if you don't mind. I don't want to further bloat the bundle since we decided to enable this in prod.

Comment thread backend/attachRendererFiber.js Outdated
_event: 'mount',
});

const isRoot = fiber.tag === 3;
Copy link
Copy Markdown
Contributor

@bvaughn bvaughn Jan 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Because of references like this and the ones in getDataFiber I wonder if it wouldn't be clearer to define a duplicate ReactTypeOfWork const in this project. At least this way if a value ever changes it's clear where all of the mirrored values are (and we aren't just greping for numbers).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed.

Comment thread backend/getDataFiber.js
context = null;
}
}
const inst = publicInstance;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the inst var needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid next lines breaking over 80 character lint :-)
I can restructure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough! 🤣

Comment thread backend/installGlobalHook.js Outdated
}
},
// Fiber-only:
supportsFiber: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this unused now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No:

if (
  typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
  typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.supportsFiber
) {

Necessary so that Fiber doesn't try to attach to non-Fiber DevTools and crash them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we release a new version of the devtools that auto-updates everyone then before we release Fiber we can revert this check with the assumption that devtools gets updated before renderers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think so.

Comment thread backend/installGlobalHook.js Outdated
}
},
onCommitFiberRoot: function(rendererID, root) {
const mountedRoots = this.getFiberRoots(rendererID);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that the use of this instead of closure capture invalidates the suggestion I had in https://github.com/facebook/react/pull/8818/files#r98101132

Comment thread backend/installGlobalHook.js Outdated
const mountedRoots = this.getFiberRoots(rendererID);
const current = root.current;
const isKnownRoot = mountedRoots.has(root);
const isUnmounting = current.memoizedState == null || current.memoizedState.element == null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @acdlite This is regard to roots but I don't really have a better idea.

We could possibly make onCommitFiberRoot accept the direct children of roots instead of the actual root but it's nice to be able to have the mount container target here too for various debug data.

Comment thread backend/attachRendererFiber.js Outdated
const prevData = getDataFiber(fiber.alternate, getOpaqueNode);
// Avoid unnecessary updates since they are common
// when something changes deep in the tree due to setState.
if (!hasDataChanged(prevData, nextData)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to do this at the fiber level instead? Maybe it's more fragile to updates but faster since you don't have to create the other objects.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by 09517f2.

Copy link
Copy Markdown
Contributor

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bunch of nits but I think conceptually this is a good start to land.

Comment thread backend/attachRendererFiber.js Outdated
prevData.ref !== nextData.ref ||
prevData.source !== nextData.source ||
prevData.props !== nextData.props ||
prevData.state !== nextData.state ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During a forceUpdate I don't think state object reference has necessarily changed and yet the component can have been mutated deep below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:-(

Copy link
Copy Markdown
Contributor

@sebmarkbage sebmarkbage Jan 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @acdlite

So, we need to know this information for life-cycles too. Currently we schedule an update effect tag that will be consumed during the commit. You might be able to use that. Ideally we would only do that if there exists a componentDidUpdate life-cycle but maybe we can keep doing that for this purpose too.

Copy link
Copy Markdown
Contributor Author

@gaearon gaearon Jan 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 09517f2, I check .updateQueue.hasForcedUpdate on the committed work and it seems to be fine. Can I rely on this?

Instead of diffing the data, we diff the fibers themselves.
We also keep track of child order changes so that we don't have to diff children separately.
This lets us bail out more efficiently.
@gaearon gaearon merged commit 4f6bec0 into master Jan 27, 2017
pastelsky pushed a commit to pastelsky/react-devtools that referenced this pull request Feb 25, 2017
* Initial support for Fiber

* Set a flag indicating Fiber-capable DevTools

React Fiber will check this flag before attempting to inject itself.
This avoids crashes on pre-Fiber DevTools version when Fiber is on the page.

* Rebuild

* Specify the missing key

* Make function naming consistent

* Attach to the tree lazily

* Implement DOM selection

* Implement updater for composites

* Avoid diffing children

* Support portals

* Rebuild

* Fix lint and flow

* The bridge is async so we may receive unmounted fibers

* Fix comment

* Compare fibers more efficiently

Instead of diffing the data, we diff the fibers themselves.
We also keep track of child order changes so that we don't have to diff children separately.
This lets us bail out more efficiently.

* _event => type

* Simplify unmounting code

* Copy ReactTypeOfWork enum

* Address feedback

* Add opaque node lookup to getReactElementFromNative()

It is necessary in cases we get a different fiber than the one we started with.

* Fix Flow
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants