Skip to content

构建Fiber对象

节点构建顺序:

  • 从最外层节点开始构建,也就是 virtualDOM 树的根节点

  • 构建完成之后接下来就开始构建两个子级节点

    注意:只有第一个子级是父级的子级,第二个子级是第一个子级下一个兄弟节点

  • 确定关系之后,再去找第一个子级节点的子节点,还是最左边的去构建这个节点的 Fiber 对象

  • 构建完成之后,再构建该子级的两个子级,然后确定他们之间的关系,确定关系之后发现没了子级,就会去找子节点的同级,按照深度遍历顺序去构建

image-20230117102813788

构建根节点 Fiber 对象

getFirstTask 就是获取任务队列中的第一个排在最前面的小任务,通过第一个小任务对象,构建根节点的 fiber 对象

对于根节点 fiber 对象不需要指定 type 属性,需要指定如下属性:

  • props:值为 task.props
  • stateNode:存储的是当前节点 DOM 对象
  • tag:是一个标记,根节点值为 host_root
  • effects:这个数组我们先不获取给一个空数组
  • 根节点不需要 effectTag 属性,因此不需要新增、修改、删除,也没有 parennt 因为这是根节点
  • child:子级节点属性值,值先指定为 null
js
const getFirstTask = () => {
  /**
   * 从任务列队获取任务
   */
  const task = taskQueue.pop()
  /**
   * 返回最外层节点的 fiber 对象
   */
  return {
    props: task.props,
    stateNode: task.dom,
    tag: 'host_root',
    effects: [],
    child: null
  }
}

一个根节点 fiber 对象构建完成

image-20230118143110068

拿到 fiber 对象之后且浏览器有空闲时间的时候,就执行 workLoop 方法里面的 while 循环,就会把根节点 fiber 对象传递给方法 executeTask方法,这样我们整个任务调度和 fiber 对象就算运作起来了

image-20230118143512130

构建子级节点 Fiber 对象

子级 fiber 对象的构建在 executeTask 方法中构建完成,executeTask 会调用 reconcileChildren 方法

  • 第一个参数为 fiber(父级 fiber 对象)
  • 第二个参数为子级 vitrualDOM 对象通过 fiber.props.children 获取

实现 reconcileChildren

reconcileChildren 这个方法第二个参数为 children,有可能是一个对象或数组

  • 当我们调用 render 方法的时候 children 是一个对象
  • 如果不是我们传的是 createElement 返回的,那么 children 就是一个数组

因为 children 可能是数组或对象,这样的参数传过来特别影响代码之后的操作,所以我们需要定义一个方法判断这个参数是否是数组

js
// Misc/Arrified/index.js
const arrified = arg => (Array.isArray(arg) ? arg : [arg])

// Reconciliation/index.js
const reconcileChildren = (fiber, children) => {
  /**
   * children 可能是对象也可能是数组,将 children 转换成数组
   */
  const arrifiedChildren = arrified(children)
}

接下来将 arrifiedChildren 数组中 virtualDOM 转换为 Fiber 对象,我们需要一个循环去将数组中的 virtualDOM 构建出一个 fiber 对象

js
const reconcileChildren = (fiber, children) => {
  /**
   * children 可能是对象也可能是数组,将 children 转换成数组
   */
  const arrifiedChildren = arrified(children)
  let index = 0
  const numberOfElements = arrifiedChildren.length
  let element = null
  let newFiber = null
  
  while (index < numberOfElements) {
    element = arrifiedChildren[index]
    newFiber = {
      type: element.type,
      props: element.props,
      tag: 'host_component',
      effects: [],
      effectTag: 'placement',
      stateNode: null,
      parent: fiber
    }

    fiber.child = newFiber
    index++
  }
}

设置 stateNode 属性和 tag 属性

这时候就完成了每个 DOM 对象构建成 Fiber 的工作。这里还有点小问题,第一个节点才是父节点,其他节点都是兄弟节点,第二个是第一个兄弟节点。所以第一个子节点才去设置子节点 child 属性,其他的都需要设置兄弟节点

  • index === 0 是子节点,其他的为彼此兄弟的节点
js
const reconcileChildren = (fiber, children) => {
  const arrifiedChildren = arrified(children)
  let index = 0
  const numberOfElements = arrifiedChildren.length
  let element = null
  let newFiber = null
  let prevFiber = null
  while (index < numberOfElements) {
    // 子级 virtualDOM 对象
    element = arrifiedChildren[index]
    // 子级 fiber 对象
    newFiber = {
      type: element.type,
      props: element.props,
      tag: getTag(element),
      effects: [],
      effectTag: 'placement',
      stateNode: null,
      parent: fiber
    }

    // 为 fiber 节点添加 DOM 对象或组件实例对象
    newFiber.stateNode = createStateNode(newFiber)

    // 为父级 fiber 添加子级 fiber
    if (index === 0) {
      fiber.child = newFiber
    } else {
      // 为 fiber 添加下一个兄弟 fiber
      prevFiber.sibling = newFiber
    }
    prevFiber = newFiber
    index++
  }
}

在处理子节点时,每一个子节点的类型都是不一样的,可以通过一个方法去判定节点的类型,通过不同类型给 tag 设置值

js
// Misc/GetTag/index.js
const getTag = vdom => {
  if (typeof vdom.type === 'string') {
    return 'host_component'
  }
}

stateNode 属性我们使用 createStateNode 函数调用去获取。需要参数是当前节点 fiber 对象 newFiber。createStateNode 函数里判断 fiber 对象 tag 为 host_component 时,则去生成 DOM 元素

js
// Misc/CreateStateNode/index.js
import { createDOMElement } from '../../DOM'
const createStateNode = fiber => {
  if (fiber.tag === 'host_component') {
    return createDOMElement(fiber)
  }
}

在 Misc 中创建文件夹为 DOM,专门对于DOM元素的一些操作都在这里

js
// DOM/createDOMElement.js
import updateNodeElement from "./updateNodeElement"
function createDOMElement(virtualDOM) {
  let newElement = null
  if (virtualDOM.type === "text") {
    // 文本节点
    newElement = document.createTextNode(virtualDOM.props.textContent)
  } else {
    // 元素节点
    newElement = document.createElement(virtualDOM.type)
    updateNodeElement(newElement, virtualDOM)
  }

  return newElement
}

构建左侧节点树中剩余节点 Fiber 对象

以上我们已经把根节点,和他的第一层子节点和第一层子节点的同级兄弟节点都已经构建 fiber 对象并且关联了起来,接下来我们要继续往下查找子节点继续构建 fiber 对象

当执行完 reconcileChildren,fiber.child 有值的话,executeTask 返回一个新的 fiber 对象,workLoop 方法里 subTask 重新被赋值继续执行 executeTask。直到 executeTask 子节点为空,结束循环

js
const executeTask = fiber => {
  reconcileChildren(fiber, fiber.props.children)
  if (fiber.child) {
    return fiber.child
  }
  console.log(fiber)
}

const workLoop = deadline => {
  while(subTask && deadline.timeRemaining() > 1) {
    subTask = executeTask(subTask)
  }
}

image-20230118151952615

构建剩余子节点的 Fiber 对象

当左侧节点构建完成之后我们定位的应该是最后一个子节点,就根据这个最后一个子节点去查找剩余节点,如果当前节点有同级就去构建该节点。如果没有就退回他的父级,查看他的父级有没有同级,一直退回查找构建 fiber 对象,将所有剩余子节点构建 fiber 对象

js
const executeTask = fiber => {
  reconcileChildren(fiber, fiber.props.children)
  if (fiber.child) {
    return fiber.child
  }
  let currentExecuteFiber = fiber
  while (currentExecuteFiber.parent) {
    // 有同级返回同级
    if (currentExecuteFiber.sibling) {
      return currentExecuteFiber.sibling
    }
    // 退到父级
    currentExecuteFiber = currentExecuteFiber.parent
  }
}

常备不懈,才能有备无患