- Scheduler【调度器】:调度任务的优先级,高优任务优先进入Reconciler
- Reconciler【协调器】:负责找出变化的组件,标识effectTag
- Renderer【渲染器】 :负责将变化的组件渲染到页面上
调度器
异步可中断:浏览器是否有剩余时间作为任务中断的标准,需要浏览器提供机制在空闲时间进行回掉。
requestIdleCallback:方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
React当中没有使用requestIdleCallback,而是自己实现的功能,这里使用requestIdleCallback代替调度器的功能。
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
requestIdleCallback会自己在空余时间回掉执行workLoop方法,加上workLoop内部调用requestIdleCallback,所以workLoop会在浏览器有空余的时间内一直进行循环回掉。
调度器和协调器调用关系
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
如果nextUnitOfWork不为空,并且浏览器存在空余时间的情况下,调用performUnitOfWork方法。这里的nextUnitOfWrok是什么呢?
nextUnitOfWork是fiber节点,每一个fiber节点。React16将以前的同步递归遍历,改成异步可中断遍历。这里的nextUnitOfWork就是每一个需要进行调和处理的节点。处理完当前节点之后,nextUnitOfWork会被赋值为下一个需要调和处理的节点。这里下一个节点什么时候进行调和,受调度器控制。
调度器和渲染器调用关系
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
假设当前fiber节点调和完毕,则会进行渲染阶段,虚拟dom映射到真实dom上。
协调器
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
调和阶段是负责找出变化的组件,标识effectTag。那主要为了对fiber树节点的遍历,对比,找出差异,打上tag。
时间复杂度为O(n)的遍历方式
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
核心在于,fiber调和阶段对于每个节点的处理,每个节点会进行父子,兄弟关系的关联。假设树节点如下,当进行深度优先遍历的时候,顺序如连接线的顺序。
如果遍历完node4之后,知道下一个节点就是node5,知道node5遍历之后,需要回到node2节点,然后下一个节点是node3。这样的话,每一个节点执行之后,可以按照规律找到下一个节点是什么。那就不用递归循环遍历了。
所以这里需要对每个节点进行链表节点处理。
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
/* fiber 节点寻找差异这部分先不看 */
/*
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber, // 着重看这一行
alternate: oldFiber,
effectTag: "UPDATE"
};
*/
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
举例:
- 当nextUnitOfWork为node1的时候
- 当nextUnitOfWork为node2的时候
- 处理完节点
如何进行节点遍历的?
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
/* 这里不关心 */
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
- node1 -》node2:因为node2是node1的第一个节点,设置了node1.child = node2。
- node4 -》node5:node2调和之后,因为node2当前节点有child节点,指向node4。node4没有child节点,所会执行nextFiber.sibling,所以node4.sibling = node5。
- node5 -》node3:node5没有child节点,也没有sibling节点,所以执行nextFiber = nextFiber.parent,nextFiber指向了node2。node2.sibling = node3。
找出差异
先了解代码里的两个变量:
- currentRoot:当前已经渲染在页面上的fiber root节点。
- wipRoot:workInProgress root,本次更新在调和阶段创建的root fiber节点。
双缓存概念
这里两个fiber树,是为了在wipRoot完成之后,直接替换现有的currentRoot,不会造成过多的等待时间。
两个阶段
首屏渲染
首屏渲染的时候currentRoot为null,wipRoot创建成功之后,直接替换currentRoot,所有的阶段的effectTag均为PLACEMENT。
这里需要关心的是下面的代码
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
deletions = [];
nextUnitOfWork = wipRoot;
}
render方法执行的时候,会将alternate属性赋值为currentRoot。因为这里是首屏渲染,所以currentRoot是null。
更新阶段
此时currentRoot已经存在,在调和阶段会进行currnetRoot树和wipRoot树进行对比。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
deletions = [];
nextUnitOfWork = wipRoot;
}
更新阶段,首屏渲染已经结束,所以currentRoot是首屏渲染的节点数据。
对比逻辑
这里的问题是如何进行每个节点和原先的节点进行处理。
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE"
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT"
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
判断分为三种类型:
- sameType:相同类型,标识 UPDATE
- element && !sameType:不同类型,新元素标识 PLACEMENT
- oldFiber && !sameType:不同类型,老元素标识 DELETION
渲染器
更新页面元素
function commitWork(fiber) {
if (!fiber) {
return;
}
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
操作的还是根节点#root。经过首屏渲染之后,current Fiber树和wip Fiber(workInProgress)之间的差异会被同步到dom节点上。
Hooks
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: []
};
const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => {
hook.state = action(hook.state);
});
const setState = action => {
hook.queue.push(action);
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
这里是将hooks作为根节点的属性进行存储。每一个更新,都会先去currentRoot查看之前的state数据,进行赋值。
const hook = {
state: oldHook ? oldHook.state : initial,
queue: []
};
当然这里的hooks处理是同步的,不会进行batchUpdate处理。
代码:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object" ? child : createTextElement(child)
)
}
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
};
}
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
updateDom(dom, {}, fiber.props);
return dom;
}
const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = (prev, next) => key => !(key in next);
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed 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 old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = "";
});
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name];
});
// 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 commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
deletions = [];
nextUnitOfWork = wipRoot;
}
let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: []
};
const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => {
hook.state = action(hook.state);
});
const setState = action => {
hook.queue.push(action);
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE"
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT"
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
const Didact = {
createElement,
render,
useState
};
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1);
const [state1, setState1] = Didact.useState(1);
return (
<div>
<h1 onClick={() => setState(c => c + 1)} style="user-select: none">
Count: {state}
</h1>
<h1 onClick={() => setState1(c => c + 1)} style="user-select: none">
Count: {state1}
</h1>
</div>
);
}
const element = <Counter />;
const container = document.getElementById("root");
Didact.render(element, container);