从零实现一个mini-react(三)commit和协调器

238 阅读3分钟

commit前导

从上文 我们实现了fiber 链表结构 这样保证 react 可以实现 更新时能够暂停,终止,复⽤渲染任务。

但是 也是有弊端的, 也就是终止时 渲染fiber dom 也是 渲染了一部分,看到的是不完整的UI

存在问题,就是每次处理一个元素,都要向DOM添加一个新的节点,在完成整个树的渲染之前,由于做了可中断操作,那将看到一个不完整的UI,这样显然是不行的

为了解决以上渲染UI不完整问题 我们要改良渲染步骤

即 将 渲染DOM 和 生成Fiber分离

  • step1 生成所有Fiber
  • step2 将fiber节点 递归生成DOM

commit实现

step1: 对fiber 直接生成DOM操作 这段代码 注销

function performUnitOfWork(fiber: FiberProps): FiberProps | null | undefined {
  ...
  // if (fiber.parent?.dom) {
  //   fiber.parent.dom.appendChild(fiber.dom)
  // }
}

step2:创建 wipRoot

创建 wipRoot(work in progress root) 在进程中的树

/**
 * 初始化第一个fiber节点
 * */ 
function render(vDom: VDOMProps , container: Element) {
  wipRoot = {
    dom: container,
    props: {
      children: [vDom],
    },
  } as FiberProps
  nextUnitOfWork = wipRoot
}

// work in progress root
let wipRoot = null as FiberProps | null | undefined;

function workLoop(deadline: any) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
   ...
  }
  // 当所有fiber都生成结束 开始 commitRoot 渲染DOM
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }
  ...
}

step3: 渲染DOM

将wipRoot 递归遍历生成DOM

// 生成节点
function commitRoot() {
  commitWork(wipRoot!.child)
  // 生成结束后 初始化 wipRoot
  wipRoot = null
}

function commitWork(fiber: FiberProps | null | undefined) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent!.dom;
  // 更新dom节点
  domParent && domParent.appendChild(fiber.dom as Element)
  // 先遍历子工作格
  commitWork(fiber.child)
  // 再遍历兄弟工作格
  commitWork(fiber.sibling)
}

以上代码地址 github.com/beewolf233/…

协调器

到目前为止,我们只实现了向DOM添加内容,所以接下来的目标我们实现更新和删除节点;

当执行更新时,我们要对比两棵fiber树(diff),对有变化的DOM进行更新 diff相关文章参考

对于fiber 数据结构发生了改变, 增加了 alternate(备胎) 用于记载 前fiber 节点 方便和 最新的节点进行diff比较

大致过程就是 同层 同位置节点进行比较

// 单个工作格类型
export type FiberProps = VDOMProps & {
  /** 真实dom节点*/
  dom: Element | null;
  /** 父节点工作格 */
  parent?: FiberProps;
  /** 子节点工作格 父节点下第一个子节点 */
  child?: FiberProps;
  /** 相邻工作格 相邻的下一个兄弟节点 */
  sibling?: FiberProps;
  /** 属性 */
  props: Omit<VDOMProps, 'children'> & {
    children: FiberProps[]
  },
  /** 前工作格流 用于对比 相当于 前一个fiber 进行diff比较 */
  alternate: FiberProps | null;
  /** 副作用 标签 */
  effectTag: 'UPDATE' | 'PLACEMENT' | 'DELETION'
}

step1 初始化 Render

// 当前fiber树
let currentRoot = null as FiberProps | null | undefined;
// 要删除的节点
let deletions = [] as FiberProps[]

/**
 * 初始化第一个fiber节点
 * */ 
function render(vDom: VDOMProps , container: Element) {
  wipRoot = {
    dom: container,
    props: {
      children: [vDom],
    },
    alternate: currentRoot,
  } as FiberProps
  nextUnitOfWork = wipRoot
  deletions = []
}

step2 diff 比较当前节点 reconcileChildren

生成fiber部分 变为方法 reconcileChildren

function performUnitOfWork(fiber: FiberProps): FiberProps | null | undefined {
  ...
  // 生成fiber
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements)
  ...
}

/** 
 * 相同层级 children 遍历 
 * diff算法就发生在 调和阶段
 * */ 
function reconcileChildren(wipFiber: FiberProps, elements: FiberProps[]) {
  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 as FiberProps | null

    const sameType =
      oldFiber &&
      element &&
      element.type === oldFiber.type
    if (sameType) {
      // TODO update the node
      newFiber = {
        type: oldFiber!.type,
        props: element.props,
        dom: oldFiber!.dom,
        parent: wipFiber,
        alternate: oldFiber!,
        effectTag: "UPDATE",
      }
    }
    if (element && !sameType) {
      // TODO add this node
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
    }
    if (oldFiber && !sameType) {
      // TODO delete the oldFiber's node
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      wipFiber.child = newFiber as FiberProps
    } else if (element) {
      // 如果有兄弟节点  返回相邻兄弟工作格
      if (prevSibling) {
        (prevSibling as FiberProps).sibling = newFiber as FiberProps
      }
    }
    prevSibling = newFiber
    index++
  }
}

step3 在commit阶段 更新DOM

// 生成节点
function commitRoot() {
  // 先删除相应要删除的节点
  deletions.forEach(commitWork)
  commitWork(wipRoot!.child)
  // 生成结束后 更新最新fiber树
  currentRoot = wipRoot
  // 生成结束后 初始化 wipRoot
  wipRoot = null
}

function commitWork(fiber: FiberProps | null | undefined) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent!.dom;
  if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    // 新建dom节点
    domParent!.appendChild(fiber.dom as Element)
  } else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    // 更新dom节点
    updateDom(
      fiber.dom,
      fiber.alternate!.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    // 删除dom节点
    domParent!.removeChild(fiber.dom as Element)
  }
  // 先遍历子工作格
  commitWork(fiber.child)
  // 再遍历兄弟工作格
  commitWork(fiber.sibling)
}

以上就完成了 协调器 增删改查Fiber DOM元素

代码地址 github.com/beewolf233/…

参考文章

手把手带你写一个mini版的React,掌握React的基本设计思想