This repository was archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Merged
Support Fiber #475
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
4a5b458
Initial support for Fiber
gaearon 0e38f59
Set a flag indicating Fiber-capable DevTools
gaearon ad8d650
Rebuild
gaearon d427de7
Specify the missing key
gaearon 718e992
Make function naming consistent
gaearon c17c6c9
Attach to the tree lazily
gaearon acf7e5f
Implement DOM selection
gaearon 2e9aef7
Implement updater for composites
gaearon 4ac6d6e
Avoid diffing children
gaearon a883f89
Support portals
gaearon 01c029c
Rebuild
gaearon 0558aae
Fix lint and flow
gaearon f43c273
The bridge is async so we may receive unmounted fibers
gaearon 504aa5e
Fix comment
gaearon 09517f2
Compare fibers more efficiently
gaearon f5229f2
_event => type
gaearon a379220
Simplify unmounting code
gaearon 97a8fff
Copy ReactTypeOfWork enum
gaearon 5a1777f
Address feedback
gaearon 03e8da2
Add opaque node lookup to getReactElementFromNative()
gaearon 8dfedaa
Fix Flow
gaearon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /** | ||
| * Copyright (c) 2015-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. | ||
| * | ||
| * @flow | ||
| */ | ||
| 'use strict'; | ||
|
|
||
| // Copied from React repo. | ||
|
|
||
| module.exports = { | ||
| IndeterminateComponent: 0, // Before we know whether it is functional or class | ||
| FunctionalComponent: 1, | ||
| ClassComponent: 2, | ||
| HostRoot: 3, // Root of a host tree. Could be nested inside another node. | ||
| HostPortal: 4, // A subtree. Could be an entry point to a different renderer. | ||
| HostComponent: 5, | ||
| HostText: 6, | ||
| CoroutineComponent: 7, | ||
| CoroutineHandlerPhase: 8, | ||
| YieldComponent: 9, | ||
| Fragment: 10, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| /** | ||
| * Copyright (c) 2015-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. | ||
| * | ||
| * @flow | ||
| */ | ||
| 'use strict'; | ||
|
|
||
| import type {Hook, ReactRenderer, Helpers} from './types'; | ||
| var getDataFiber = require('./getDataFiber'); | ||
| var { | ||
| ClassComponent, | ||
| HostRoot, | ||
| } = require('./ReactTypeOfWork'); | ||
|
|
||
| function attachRendererFiber(hook: Hook, rid: string, renderer: ReactRenderer): Helpers { | ||
| // This is a slightly annoying indirection. | ||
| // It is currently necessary because DevTools wants | ||
| // to use unique objects as keys for instances. | ||
| // However fibers have two versions. | ||
| // We use this set to remember first encountered fiber for | ||
| // each conceptual instance. | ||
| const opaqueNodes = new Set(); | ||
| function getOpaqueNode(fiber) { | ||
| if (opaqueNodes.has(fiber)) { | ||
| return fiber; | ||
| } | ||
| const {alternate} = fiber; | ||
| if (alternate != null && opaqueNodes.has(alternate)) { | ||
| return alternate; | ||
| } | ||
| opaqueNodes.add(fiber); | ||
| return fiber; | ||
| } | ||
|
|
||
| function hasDataChanged(prevFiber, nextFiber) { | ||
| if (prevFiber.tag === ClassComponent) { | ||
| // Only classes have context. | ||
| if (prevFiber.stateNode.context !== nextFiber.stateNode.context) { | ||
| return true; | ||
| } | ||
| // Force updating won't update state or props. | ||
| if (nextFiber.updateQueue != null && nextFiber.updateQueue.hasForceUpdate) { | ||
| return true; | ||
| } | ||
| } | ||
| // Compare the fields that would result in observable changes in DevTools. | ||
| // We don't compare type, tag, index, and key, because these are known to match. | ||
| return ( | ||
| prevFiber.memoizedProps !== nextFiber.memoizedProps || | ||
| prevFiber.memoizedState !== nextFiber.memoizedState || | ||
| prevFiber.ref !== nextFiber.ref || | ||
| prevFiber._debugSource !== nextFiber._debugSource | ||
| ); | ||
| } | ||
|
|
||
| let pendingEvents = []; | ||
|
|
||
| function flushPendingEvents() { | ||
| const events = pendingEvents; | ||
| pendingEvents = []; | ||
| for (let i = 0; i < events.length; i++) { | ||
| const event = events[i]; | ||
| hook.emit(event.type, event); | ||
| } | ||
| } | ||
|
|
||
| function enqueueMount(fiber) { | ||
| pendingEvents.push({ | ||
| // TODO: the naming is confusing. `element` is *not* a React element. It is an opaque ID. | ||
| element: getOpaqueNode(fiber), | ||
| data: getDataFiber(fiber, getOpaqueNode), | ||
| renderer: rid, | ||
| type: 'mount', | ||
| }); | ||
|
|
||
| const isRoot = fiber.tag === HostRoot; | ||
| if (isRoot) { | ||
| pendingEvents.push({ | ||
| element: getOpaqueNode(fiber), | ||
| renderer: rid, | ||
| type: 'root', | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| function enqueueUpdateIfNecessary(fiber, hasChildOrderChanged) { | ||
| if (!hasChildOrderChanged && !hasDataChanged(fiber.alternate, fiber)) { | ||
| return; | ||
| } | ||
| pendingEvents.push({ | ||
| element: getOpaqueNode(fiber), | ||
| data: getDataFiber(fiber, getOpaqueNode), | ||
| renderer: rid, | ||
| type: 'update', | ||
| }); | ||
| } | ||
|
|
||
| function enqueueUnmount(fiber) { | ||
| const isRoot = fiber.tag === HostRoot; | ||
| const opaqueNode = getOpaqueNode(fiber); | ||
| const event = { | ||
| element: opaqueNode, | ||
| renderer: rid, | ||
| type: 'unmount', | ||
| }; | ||
| if (isRoot) { | ||
| pendingEvents.push(event); | ||
| } else { | ||
| // Non-root fibers are deleted during the commit phase. | ||
| // They are deleted in the child-first order. However | ||
| // DevTools currently expects deletions to be parent-first. | ||
| // This is why we unshift deletions rather than push them. | ||
| pendingEvents.unshift(event); | ||
| } | ||
| opaqueNodes.delete(opaqueNode); | ||
| } | ||
|
|
||
| function mountFiber(fiber) { | ||
| // Depth-first. | ||
| // Logs mounting of children first, parents later. | ||
| let node = fiber; | ||
| outer: while (true) { | ||
| if (node.child) { | ||
| node.child.return = node; | ||
| node = node.child; | ||
| continue; | ||
| } | ||
| enqueueMount(node); | ||
| if (node == fiber) { | ||
| return; | ||
| } | ||
| if (node.sibling) { | ||
| node.sibling.return = node.return; | ||
| node = node.sibling; | ||
| continue; | ||
| } | ||
| while (node.return) { | ||
| node = node.return; | ||
| enqueueMount(node); | ||
| if (node == fiber) { | ||
| return; | ||
| } | ||
| if (node.sibling) { | ||
| node.sibling.return = node.return; | ||
| node = node.sibling; | ||
| continue outer; | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| function updateFiber(nextFiber, prevFiber) { | ||
| let hasChildOrderChanged = false; | ||
| if (nextFiber.child !== prevFiber.child) { | ||
| // If the first child is different, we need to traverse them. | ||
| // Each next child will be either a new child (mount) or an alternate (update). | ||
| let nextChild = nextFiber.child; | ||
| let prevChildAtSameIndex = prevFiber.child; | ||
| while (nextChild) { | ||
| // We already know children will be referentially different because | ||
| // they are either new mounts or alternates of previous children. | ||
| // Schedule updates and mounts depending on whether alternates exist. | ||
| // We don't track deletions here because they are reported separately. | ||
| if (nextChild.alternate) { | ||
| const prevChild = nextChild.alternate; | ||
| updateFiber(nextChild, prevChild); | ||
| // However we also keep track if the order of the children matches | ||
| // the previous order. They are always different referentially, but | ||
| // if the instances line up conceptually we'll want to know that. | ||
| if (!hasChildOrderChanged && prevChild !== prevChildAtSameIndex) { | ||
| hasChildOrderChanged = true; | ||
| } | ||
| } else { | ||
| mountFiber(nextChild); | ||
| if (!hasChildOrderChanged) { | ||
| hasChildOrderChanged = true; | ||
| } | ||
| } | ||
| // Try the next child. | ||
| nextChild = nextChild.sibling; | ||
| // Advance the pointer in the previous list so that we can | ||
| // keep comparing if they line up. | ||
| if (!hasChildOrderChanged && prevChildAtSameIndex != null) { | ||
| prevChildAtSameIndex = prevChildAtSameIndex.sibling; | ||
| } | ||
| } | ||
| // If we have no more children, but used to, they don't line up. | ||
| if (!hasChildOrderChanged && prevChildAtSameIndex != null) { | ||
| hasChildOrderChanged = true; | ||
| } | ||
| } | ||
| enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged); | ||
| } | ||
|
|
||
| function walkTree() { | ||
| hook.getFiberRoots(rid).forEach(root => { | ||
| // Hydrate all the roots for the first time. | ||
| mountFiber(root.current); | ||
| }); | ||
| flushPendingEvents(); | ||
| } | ||
|
|
||
| function cleanup() { | ||
| // We don't patch any methods so there is no cleanup. | ||
| } | ||
|
|
||
| function handleCommitFiberUnmount(fiber) { | ||
| // This is not recursive. | ||
| // We can't traverse fibers after unmounting so instead | ||
| // we rely on React telling us about each unmount. | ||
| // It will be flushed after the root is committed. | ||
| enqueueUnmount(fiber); | ||
| } | ||
|
|
||
| function handleCommitFiberRoot(root) { | ||
| const current = root.current; | ||
| const alternate = current.alternate; | ||
| if (alternate) { | ||
| // TODO: relying on this seems a bit fishy. | ||
| const wasMounted = alternate.memoizedState != null && alternate.memoizedState.element != null; | ||
| const isMounted = current.memoizedState != null && current.memoizedState.element != null; | ||
| if (!wasMounted && isMounted) { | ||
| // Mount a new root. | ||
| mountFiber(current); | ||
| } else if (wasMounted && isMounted) { | ||
| // Update an existing root. | ||
| updateFiber(current, alternate); | ||
| } else if (wasMounted && !isMounted) { | ||
| // Unmount an existing root. | ||
| enqueueUnmount(current); | ||
| } | ||
| } else { | ||
| // Mount a new root. | ||
| mountFiber(current); | ||
| } | ||
| // We're done here. | ||
| flushPendingEvents(); | ||
| } | ||
|
|
||
| // The naming is confusing. | ||
| // They deal with opaque nodes (fibers), not elements. | ||
| function getNativeFromReactElement(fiber) { | ||
| try { | ||
| const opaqueNode = fiber; | ||
| const hostInstance = renderer.findHostInstanceByFiber(opaqueNode); | ||
| return hostInstance; | ||
| } catch (err) { | ||
| // The fiber might have unmounted by now. | ||
| return null; | ||
| } | ||
| } | ||
| function getReactElementFromNative(hostInstance) { | ||
| const fiber = renderer.findFiberByHostInstance(hostInstance); | ||
| if (fiber != null) { | ||
| // TODO: type fibers. | ||
| const opaqueNode = getOpaqueNode((fiber: any)); | ||
| return opaqueNode; | ||
| } | ||
| return null; | ||
| } | ||
| return { | ||
| getNativeFromReactElement, | ||
| getReactElementFromNative, | ||
| handleCommitFiberRoot, | ||
| handleCommitFiberUnmount, | ||
| cleanup, | ||
| walkTree, | ||
| }; | ||
| } | ||
|
|
||
| module.exports = attachRendererFiber; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
returnpointers? That doesn't seem right. Am I reading this wrong? (Also when wouldnode.child.returnever not equalnode?)There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 expect these
returnassignments in devtools are basically neutral since the pointers should be correct to begin with, but it still seems weird to see something outside ofReactsetting values on fibers.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
returnI think. However, ifreturndoesn't match, we risk getting an infinite loop.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.