一、为什么需要从vdom树到fiber树?
(1)Stack reconcile不可打断递归渲染 VS fiber 可打断链
因为stack reconciler递归渲染 vdom 可能耗时很多,JS 计算量大了会阻塞渲染,不可打断。
// vdom变更前
{
type: "div",
props: {
children: [
{
type: "TEXT_ELEMENT",
props: {
nodeValue: 'carter',
children: [],
},
}
],
},
}
// vdom变更后
{
type: "div",
props: {
children: [
{
type: "TEXT_ELEMENT",
props: {
nodeValue: 'new text',
children: [],
},
},
{
type: "span",
props: {
children: [
{
type: "TEXT_ELEMENT",
props: {
nodeValue: '这是新增的span text',
children: [],
},
},
],
},
}
],
},
}
fiber 是可打断的,就不会阻塞渲染,而且还会在这个过程中把需要用到的 dom 创建好,做好 diff 来确定是增是删还是改。
dom 有了,增删改也知道了咋做了,一次性 commit 很快了。
(2)数据结构设计
vdom数据结构
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
}
}
}
fiber的数据结构与之前的vdom有所新增几个属性:
(1) fiber拥有child、sibling、return 节点关系属性
(2) effeactTag 表示该节点是增/删/改。
type: element.type,
props: element.props, // fiber.props.children 就是 vdom 的子节点
dom: null, // fiber.dom属性表示本节点的 dom元素。
return: wipFiber, // 根节点的return为空
// 循环处理每一个 vdom 的 children elements,
// 如果 index 是 0,那就是 child 串联
// 否则是 sibling 串联
child?: fiber //可能有此属性
sibling?: fiber // 可能有此属性
effectTag: "PLACEMENT" | "UPDATE" | "DELETION",
孩子兄弟表示法,类似二叉树表示法。加上了return指向parent。
二、schedule
它就是一个不断的循环,就像 event loop 一样,可以叫做 reconcile loop。
然后它做的事情就是 vdom 转 fiber,也就是 reconcile。
let nextFiberReconcileWork = null;
let wipRoot = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) { // 调度转换fiber
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextFiberReconcileWork && wipRoot) { // 转换完成,进行commit渲染
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
}
}
nextFiberReconcileWork = wipRoot
}
render(jsx, document.getElementById("root"));
三、reconcile
vdom转为fiber,首先由DFS也就是深度遍历到叶子节点,其次再广度遍历BFS到兄弟节点。也就是先纵向到底,再横向到底。
function performNextWork(fiber) {
reconcile(fiber);
// 优先reconcile child子节点(纵向,从顶而下)
if (fiber.child) {
return fiber.child;
}
// 其次reconcile sibling兄弟节点(横向,从左到右)
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return; // nextFiber的纵向和横向都走完了,则回到父节点上
}
// 最后会执行到根节点, nextFiber 为root.return的时候,返回undefined,表示所有节点reconcile完成,可以进行下一步也就是commit渲染到浏览器上。
// wookLoop 函数中这一句
// if (!nextFiberReconcileWork && wipRoot) {
// commitRoot();
// }
}
fiber.props.children 就是 vdom 的子节点,这里的 reconcileChildren 就是把之前的 vdom 转成 child、sibling、return 这样串联起来的 fiber 链表。
于此同时,提前创建dom节点保存在fiber中。
同时,还需要标记增加 、更新、删除。
function reconcile(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let prevSibling = null
while (
index < elements.length
) {
const element = elements[index]
let newFiber = { // 此处简化,暴力替换。重点关注return\child\sibling节点关系
type: element.type,
props: element.props,
dom: null,
return: wipFiber,
effectTag: "PLACEMENT",
}
if (index === 0) { // index为0的时候,使用child连接父节点;
wipFiber.child = newFiber
} else if (element) { // 其他index值,使用sibling 连接兄弟节点
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
vdom转为fiber,首先由DFS也就是深度遍历到叶子节点,其次再广度遍历BFS到兄弟节点。也就是先纵向到底,再横向到底。
第一次执行reconcile,传入wipRoot 这个vdom, 同时将wipRoot.props.children都转化为fiber,也就是生成ul这个fiber, 建立child、sibling、return关联关系:
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
}
}
nextFiberReconcileWork = wipRoot
}
第二次,执行reconcile,传入ul的fiber,同时将ul的子节点生成fiber,建立child、sibling、return关联关系。
第三次, 执行reconcile,传入第一个li的fiber,同时将li的children转为fiber,建立child、sibling、return关联关系
第四次,执行reconcile,传入aa这个fiber,没有children。
第五次,执行reconcile,aa没有child,也没有sibling,则找寻父节点return到第一个li中,横向找到第二个Li,建立child、sibling、return关联关系。
四、commit
一次性将所有的dom增删改,渲染在页面上。
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.return
while (!domParentFiber.dom) { // 为什么需要这个while??to find the parent of a DOM node we’ll need to go up the fiber tree until we find a fiber with a DOM node.
// A:函数组件 需要递归找到return父节点。函数组件可能使用 fragment <></>, 这时候fiber.dom是不存在的
domParentFiber = domParentFiber.return
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
有别于vue和React stack版本,vue是在diff过程中,就通过增加、移除、挪动和更新完成dom操作。
五、hooks useState实现
一个计算组件使用useState:
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
useState简易实现:
function useState(initial) {
// 获取上次渲染时候,组件的 hook以及相应的state信息
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
// 将setState的事件队列queue全部执行,batch执行;action是setState(c => c + 1) 是 指c => c + 1这样的函数。
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action) // 存储useState
// setState触发schedule调度执行reconcile(wipRoot)
// 为什么取currentRoot为上次的wipRoot??? 简化心智模型!方便找到上一次与本次fiber
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
React Hooks有batchedUpdates,当在click中触发三次updateNum,精简React mini会触发三次更新,而React只会触发一次