React mini --fiber简易实现

77 阅读4分钟

一、为什么需要从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 HooksbatchedUpdates,当在click中触发三次updateNum精简React mini会触发三次更新,而React只会触发一次