Skip to content

Instantly share code, notes, and snippets.

@gnosis23
Created August 11, 2019 13:23
Show Gist options
  • Save gnosis23/dad32b3c7130166f24b821823c7a6eb9 to your computer and use it in GitHub Desktop.
Save gnosis23/dad32b3c7130166f24b821823c7a6eb9 to your computer and use it in GitHub Desktop.
fiber demo
// ============================================================================
// 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