Created
August 11, 2019 13:23
-
-
Save gnosis23/dad32b3c7130166f24b821823c7a6eb9 to your computer and use it in GitHub Desktop.
fiber demo
This file contains 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
// ============================================================================ | |
// types | |
// ============================================================================ | |
declare var requestIdleCallback: any; | |
enum FiberType { | |
HOST_ROOT, | |
HOST_COMPONENT, | |
CLASS_COMPONENT, | |
} | |
enum EffectTag { | |
PLACEMENT, | |
DELETION, | |
UPDATE, | |
} | |
interface ReactElement { | |
type: typeof Component | string; | |
props: any; | |
}; | |
interface FiberDOMElement extends HTMLElement { | |
__fiber?: FiberNode; | |
_rootContainerFiber?: FiberNode; | |
} | |
interface FiberNode { | |
tag: FiberType; | |
type?: typeof Component | string; | |
stateNode?: FiberDOMElement | Text | Component; | |
props: { | |
children: any; | |
}; | |
child?: FiberNode; | |
sibling?: FiberNode; | |
parent?: FiberNode; | |
alternate?: FiberNode; | |
effectTag?: EffectTag; | |
effects?: FiberNode[]; | |
partialState?: object; | |
} | |
interface MOUNT_MSG { | |
from: FiberType.HOST_ROOT, | |
dom: FiberDOMElement, | |
newProps: { children: ReactElement[] } | |
} | |
interface UPDATE_MSG { | |
from: FiberType.CLASS_COMPONENT, | |
instance: Component, | |
partialState: object, | |
} | |
type QUEUE_MSG = MOUNT_MSG | UPDATE_MSG; | |
// ============================================================================ | |
// element.js | |
// ============================================================================ | |
const TEXT_ELEMENT = 'text'; | |
function createElement(type, config, ...args): ReactElement { | |
const props = Object.assign({}, config); | |
const hasChildren = args.length > 0; | |
const rawChildren = hasChildren ? [...args] : []; | |
props.children = rawChildren | |
.filter(c => c != null && c !== false) | |
.map(c => c instanceof Object ? c : createTextElement(c)); | |
return { type, props }; | |
} | |
function createTextElement(value): ReactElement { | |
return createElement(TEXT_ELEMENT, { nodeValue: value }); | |
} | |
// ============================================================================ | |
// dom-utils.js | |
// ============================================================================ | |
const isEvent = name => name.startsWith("on"); | |
const isAttribute = name => | |
!isEvent(name) && name != "children" && name != "style"; | |
const isNew = (prev, next) => key => prev[key] !== next[key]; | |
const isGone = (prev, next) => key => !(key in next); | |
function updateDomProperties(dom, prevProps, nextProps) { | |
// Remove event listeners | |
Object.keys(prevProps) | |
.filter(isEvent) | |
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key)) | |
.forEach(name => { | |
const eventType = name.toLowerCase().substring(2); | |
dom.removeEventListener(eventType, prevProps[name]); | |
}); | |
// Remove attributes | |
Object.keys(prevProps) | |
.filter(isAttribute) | |
.filter(isGone(prevProps, nextProps)) | |
.forEach(name => { | |
dom[name] = null; | |
}); | |
// Set attributes | |
Object.keys(nextProps) | |
.filter(isAttribute) | |
.filter(isNew(prevProps, nextProps)) | |
.forEach(name => { | |
dom[name] = nextProps[name]; | |
}); | |
// Set style | |
prevProps.style = prevProps.style || {}; | |
nextProps.style = nextProps.style || {}; | |
Object.keys(nextProps.style) | |
.filter(isNew(prevProps.style, nextProps.style)) | |
.forEach(key => { | |
dom.style[key] = nextProps.style[key]; | |
}); | |
Object.keys(prevProps.style) | |
.filter(isGone(prevProps.style, nextProps.style)) | |
.forEach(key => { | |
dom.style[key] = ""; | |
}); | |
// Add event listeners | |
Object.keys(nextProps) | |
.filter(isEvent) | |
.filter(isNew(prevProps, nextProps)) | |
.forEach(name => { | |
const eventType = name.toLowerCase().substring(2); | |
dom.addEventListener(eventType, nextProps[name]); | |
}); | |
} | |
function createDomElement(fiber: FiberNode): HTMLElement | Text { | |
const isTextElement = fiber.type === TEXT_ELEMENT; | |
const dom = isTextElement | |
? document.createTextNode("") | |
: document.createElement(fiber.type as string); | |
updateDomProperties(dom, [], fiber.props); | |
return dom; | |
} | |
// ============================================================================ | |
// base-component.js | |
// ============================================================================ | |
class Component { | |
props: object; | |
state: object; | |
render: () => ReactElement | ReactElement[]; | |
__fiber?: FiberNode; | |
constructor(props) { | |
this.props = props || {}; | |
this.state = this.state || {}; | |
} | |
setState(partialState) { | |
scheduleUpdate(this, partialState); | |
} | |
} | |
function createInstance(fiber: FiberNode) { | |
const Comp = fiber.type as (typeof Component); | |
const instance = new Comp(fiber.props); | |
instance.__fiber = fiber; | |
return instance; | |
} | |
// ============================================================================ | |
// render() & scheduleUpdate() | |
// ============================================================================ | |
// Global state | |
const updateQueue: QUEUE_MSG[] = []; | |
let nextUnitOfWork: FiberNode = null; | |
let pendingCommit: FiberNode = null; | |
function render(elements, containerDom) { | |
updateQueue.push({ | |
from: FiberType.HOST_ROOT, | |
dom: containerDom, | |
newProps: { children: elements } | |
}); | |
requestIdleCallback(performWork); | |
} | |
function scheduleUpdate(instance, partialState) { | |
updateQueue.push({ | |
from: FiberType.CLASS_COMPONENT, | |
instance, | |
partialState, | |
}); | |
requestIdleCallback(performWork); | |
} | |
// ============================================================================ | |
// performWork() & workLoop() | |
// ============================================================================ | |
const ENOUGH_TIME = 1; // milliseconds | |
function performWork(deadline) { | |
workLoop(deadline); | |
if (nextUnitOfWork || updateQueue.length > 0) { | |
requestIdleCallback(performWork); | |
} | |
} | |
function workLoop(deadline) { | |
if (!nextUnitOfWork) { | |
resetNextUnitOfWork(); | |
} | |
// If the deadline is too close, it stops the work loop and leaves | |
// `nextUnitOfWork` updated so it can be resumed the next time. | |
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) { | |
// `performUnitOfWork` will build the work-in-progress tree for | |
// the update it's working on and also find out what changes | |
// we need to apply to the DOM. | |
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); | |
} | |
// when `performUnitOfWork` finishes all the work for the current update | |
// it returns null and leaves the pending changes to the DOM in `pendingCommit` | |
// Finally `commitAllWork` will take the `effects` and from `pendingCommit` | |
// and mutate the DOM. | |
if (pendingCommit) { | |
commitAllWork(pendingCommit); | |
} | |
} | |
// ============================================================================ | |
// resetUnitOfWork | |
// ============================================================================ | |
function resetNextUnitOfWork() { | |
const update = updateQueue.shift(); | |
if (!update) { | |
return; | |
} | |
// If the update has a partialState we store it on the fiber that belongs to the instance | |
// of the component, so we can use it later when we call component's `render()` | |
if ((update as UPDATE_MSG).partialState) { | |
(update as UPDATE_MSG).instance.__fiber.partialState = (update as UPDATE_MSG).partialState; | |
} | |
const root: FiberNode = | |
update.from == FiberType.HOST_ROOT | |
? update.dom._rootContainerFiber // null first time | |
: getRoot(update.instance.__fiber); // find a fiber without `parent` | |
nextUnitOfWork = { | |
tag: FiberType.HOST_ROOT, | |
stateNode: (update as MOUNT_MSG).dom || root.stateNode, | |
props: (update as MOUNT_MSG).newProps || root.props, | |
alternate: root | |
}; | |
} | |
function getRoot(fiber: FiberNode): FiberNode { | |
let node = fiber; | |
while (node.parent) { | |
node = node.parent; | |
} | |
return node; | |
} | |
// ============================================================================ | |
// performUnitOfWork | |
// ============================================================================ | |
function performUnitOfWork(wipFiber: FiberNode): FiberNode | undefined { | |
// call `beginWork` to create the new children of a fiber and then | |
// return the first child so it becomes the `nextUnitOfWork` | |
beginWork(wipFiber); | |
if (wipFiber.child) { | |
return wipFiber.child; | |
} | |
// If there isn't any child, we call completeWork and return the sibling | |
// as the `nextUnitOfWork` | |
let uow = wipFiber; | |
while (uow) { | |
completeWork(uow); | |
if (uow.sibling) { | |
return uow.sibling; | |
} | |
// If there isn't any `sibling`, we go up to the parents calling `completeWork` | |
// until we find a `sibling` or we each the root | |
uow = uow.parent; | |
} | |
} | |
// ============================================================================ | |
// beginWork(), updateHostComponent(), updateClassComponent() | |
// ============================================================================ | |
// `beginWork` does two things | |
// 1: create the `stateNode` if we don't have one | |
// 2: get the component children and pass them to `reconcileChildrenArray()` | |
function beginWork(wipFiber: FiberNode) { | |
if (wipFiber.tag == FiberType.CLASS_COMPONENT) { | |
updateClassComponent(wipFiber); | |
} else { | |
updateHostComponent(wipFiber); | |
} | |
} | |
function updateHostComponent(wipFiber: FiberNode) { | |
// create a new DOM node if it needs to without children. | |
if (!wipFiber.stateNode) { | |
wipFiber.stateNode = createDomElement(wipFiber); | |
} | |
const newChildElements = wipFiber.props.children; | |
reconcileChildrenArray(wipFiber, newChildElements); | |
} | |
function updateClassComponent(wipFiber: FiberNode) { | |
let instance = wipFiber.stateNode as Component; | |
if (instance == null) { | |
instance = wipFiber.stateNode = createInstance(wipFiber); | |
} else if (wipFiber.props == instance.props && !wipFiber.partialState) { | |
// No need to render, clone children from last time | |
cloneChildFibers(wipFiber); | |
return; | |
} | |
instance.props = wipFiber.props; | |
instance.state = Object.assign({}, instance.state, wipFiber.partialState); | |
wipFiber.partialState = null; | |
const newChildElements = (wipFiber.stateNode as Component).render(); | |
reconcileChildrenArray(wipFiber, newChildElements); | |
} | |
// ============================================================================ | |
// reconcileChildrenArray() | |
// ============================================================================ | |
function arrify(val): ReactElement[] { | |
return val == null ? [] : Array.isArray(val) ? val : [val]; | |
} | |
function reconcileChildrenArray( | |
wipFiber: FiberNode, | |
newChildrenElements: ReactElement[] | ReactElement | undefined | |
) { | |
const elements = arrify(newChildrenElements); | |
let index = 0; | |
// get the first child of alternate | |
let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null; | |
let newFiber: FiberNode = null; | |
while (index < elements.length || oldFiber != null) { | |
const prevFiber = newFiber; | |
const element = index < elements.length && elements[index]; | |
const sameType = oldFiber && element && element.type == oldFiber.type; | |
if (sameType) { | |
newFiber = { | |
type: oldFiber.type, | |
tag: oldFiber.tag, | |
stateNode: oldFiber.stateNode, | |
props: element.props, | |
parent: wipFiber, | |
alternate: oldFiber, | |
partialState: oldFiber.partialState, | |
effectTag: EffectTag.UPDATE | |
}; | |
} | |
// below not same type | |
// insert new fiber | |
if (element && !sameType) { | |
newFiber = { | |
type: element.type, | |
tag: typeof element.type === 'string' | |
? FiberType.HOST_COMPONENT : FiberType.CLASS_COMPONENT, | |
props: element.props, | |
parent: wipFiber, | |
effectTag: EffectTag.PLACEMENT, | |
}; | |
} | |
// remove old fiber | |
// 孩子不用清理吗? | |
if (oldFiber && !sameType) { | |
oldFiber.effectTag = EffectTag.DELETION; | |
wipFiber.effects = wipFiber.effects || []; | |
wipFiber.effects.push(oldFiber); | |
} | |
if (oldFiber) { | |
oldFiber = oldFiber.sibling; | |
} | |
if (index === 0) { | |
wipFiber.child = newFiber; | |
} else if (prevFiber && element) { | |
prevFiber.sibling = newFiber; | |
} | |
index++; | |
} | |
} | |
// ============================================================================ | |
// cloneChildFibers | |
// ============================================================================ | |
function cloneChildFibers(parentFiber: FiberNode) { | |
const oldFiber = parentFiber.alternate; | |
if (!oldFiber.child) { | |
return; | |
} | |
let oldChild = oldFiber.child; | |
let prevChild = null; | |
while (oldChild) { | |
const newChild = { | |
type: oldChild.type, | |
tag: oldChild.tag, | |
stateNode: oldChild.stateNode, | |
props: oldChild.props, | |
partialState: oldChild.partialState, | |
alternate: oldChild, | |
parent: parentFiber | |
}; | |
if (prevChild) { | |
prevChild.sibling = newChild; | |
} else { | |
parentFiber.child = newChild; | |
} | |
prevChild = newChild; | |
oldChild = oldChild.sibling; | |
} | |
} | |
// ============================================================================ | |
// completeWork() | |
// ============================================================================ | |
function completeWork(fiber: FiberNode) { | |
if (fiber.tag === FiberType.CLASS_COMPONENT) { | |
(fiber.stateNode as FiberDOMElement).__fiber = fiber; | |
} | |
if (fiber.parent) { | |
// 不用清理吗? | |
const childEffects = fiber.effects || []; | |
const thisEffect = fiber.effectTag != null ? [fiber] : []; | |
const parentEffects = fiber.parent.effects || []; | |
fiber.parent.effects = parentEffects.concat(childEffects, thisEffect); | |
} else { | |
pendingCommit = fiber; | |
} | |
} | |
// ============================================================================ | |
// commitAllWork() & commitWork() | |
// ============================================================================ | |
function commitAllWork(fiber: FiberNode) { | |
fiber.effects.forEach(f => { | |
commitWork(f); | |
}); | |
(fiber.stateNode as FiberDOMElement)._rootContainerFiber = fiber; | |
nextUnitOfWork = null; | |
pendingCommit = null; | |
} | |
function commitWork(fiber: FiberNode) { | |
if (fiber.tag == FiberType.HOST_ROOT) { | |
return; | |
} | |
let domParentFiber = fiber.parent; | |
while (domParentFiber.tag == FiberType.CLASS_COMPONENT) { | |
domParentFiber = domParentFiber.parent; | |
} | |
const domParent = domParentFiber.stateNode; | |
if (fiber.effectTag == EffectTag.PLACEMENT && fiber.tag == FiberType.HOST_COMPONENT) { | |
(domParent as HTMLElement).appendChild(fiber.stateNode as HTMLElement); | |
} else if (fiber.effectTag == EffectTag.UPDATE) { | |
updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props); | |
} else if (fiber.effectTag == EffectTag.DELETION) { | |
commitDeletion(fiber, domParent); | |
} | |
} | |
function commitDeletion(fiber, domParent) { | |
let node = fiber; | |
while (true) { | |
if (node.tag == FiberType.CLASS_COMPONENT) { | |
node = node.child; | |
continue; | |
} | |
domParent.removeChild(node.stateNode); | |
while (node != fiber && !node.sibling) { | |
node = node.parent; | |
} | |
if (node == fiber) { | |
return; | |
} | |
node = node.slibling; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment