React学习第十天---React & Fiber(构建Fiber对象)(三)

1,177 阅读9分钟

这是我参与更文挑战的第24天,活动详情查看: 更文挑战

前言

大家好我是小村儿,上一节我们实现了创建任务队列,添加任务,使用requestIdelCallbackapi利用浏览器空闲时间实现任务的调度逻辑。接下来我们用代码构建一个Fiber对象。

  • 构建根节点Fiber对象
  • 构建子级节点Fiber对象
  • 完善Fiber对象的stateNode和tag属性
  • 构建左侧节点数中的剩余子节点Fiber对象
  • 构建剩余子节点的fiber对象

构建根节点Fiber对象

接上一章,我们最后完成任务调度之后需要去执行任务了,这个任务是什么样的任务呢?这个任务就是为每个根据节点VirtualDOM对象构建Fiber对象

这个任务应该被怎样去执行呢?或者说我们要从哪一个节点开始构建?节点构建顺序又是怎样的?看下图:

image.png

我们是从最外层去开始构建的,也就是VirtualDOM树的根节点,构建完成后之后接下来就开始接下来的两个子级节点,构建完之后就会去指定这三个组件之间的关系,只有从左第一个子级才是父级的子级(child),从左往右算第二个就算第一个子级的下一个兄弟节点这样来构建。确定关系之后,再去找第一个子级节点的子节点,还是最左边的去构建这个节点的Fiber对象,构建完成后之后,在构建该子级的两个子级,然后确定他们之间的关系,确定关系之后发现没了子级,就会去找子节点的同级,按照深度遍历顺序去构建。

我们来尝试构建根节点的Fiber对象,也就是id为root的节点,他的子节点就是我们jsx里面最外层div包着的就是子节点。

<div>
  <p>Hello React</p>
</div>

我们先回顾一下任务调度的流程,当调用render方法的时候,我们给任务队列添加了一个对象,有两个属性一个是dom,目前根节点则是id为root的根节点 一个是props.值是一个对象,这个对象children属性,他的值就是父级的子级节点。在浏览器空闲的时候呢就会去执行performTask这个代码 requestIdelCallback(performTask).在执行performTask方法的时候回去调用workLoop这个方法,这个方法就开始去执行任务了,首先回去查看一下subTask是否有值,在第一次执行的时候subTask值为空,这时候就会去调用getFirstTask方法获取任务。

getFirstTask这个方法我们之前只做了一个函数初始化,我们现在来完成他。getFirstTask就是获取任务队列中的第一个排在最前面的小任务,通过第一个小任务对象,构建根节点的fiber对象.

// fiber对象基本结构

{
    type        节点类型(元素,文本,组件)(具体的类型)
    props       加点属性
    stateNode   节点DOM对象 | 组件实例对象
    tag         节点标记(对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)
    effects     数组,存储需要更改的fiber对象
    effectTag   当前Fiber要被执行的操作(新增,删除,修改)
    parent      当前Fiber的下一个父级Fiber
    child       当前Fiber的下一个子级Fiber
    sibling     当前Fiber的下一个兄弟Fiber
    alternate   Fiber 备份 fiber比对时使用
}

对于根节点fiber对象不需要指定type属性,我们直接构建props属性,值为task.props.stateNode属性存储的是当前节点DOM对象,tag是一个标记,根节点值为”host_root“,接下来是effects数组,这个数组我们先不去获取暂时给一个空数组为值,暂时还用不到。根节点也不需要用effectTag属性,因为不需要新增删除和修改。也没有parent因为这是根节点,最后还需要配置上一个child子级节点属性,值当前没有先指定为null,关于alternate在对比时候再用,现在暂时不指定。

const getFirstTask = () => {
  /**
   * 从任务队列中获取任务
   */
  const task = taskQueue.pop()
  /**
   * 返回最外层节点的fiber对象
   */
  return {
    props: task.props,
    stateNode: task.dom,
    tag: "host_root",
    effects: [],
    child: null
  }
}

这个对象返回之后就赋值给subTask

if(!subTask) {
    subTask = getFirstTask()
    console.log(subTask)
}

效果:

image.png 一个根节点fiber对象构建完成!!!

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

const workLoop = deadline => {
  if(!subTask) {
    // 这里的subTask就是一个fiber对象
    subTask = getFirstTask()
  }

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

现在就知道executeTask的形参被命名为fiber了。因为得到的就是根节点的fiber对象

构建子级节点Fiber对象

子级节点FIber对象的构建在executeTask方法中构建完成,executeTask会调用reconcileChildren方法,第一个参数为fiber(父级fiber对象),第二个参数为子级VitrualDOM对象通过fiber.props.children获取,

1. 实现reconcileChildren方法

关于这个方法第二个参数为children,有可能是一个对象有可能是一个数组,当我们调用render方法的时候children,我们传element是一个对象那就是一个对象,如果不是我们传的是createElement返回的那么children就是一个数组

export const render = (element, dom) => {
 taskQueue.push({
   dom,
   props: {children: element} // 这个element是对象
 })
 
 createElement.js
 // children是一个数组
 export default function createElement(type, props, ...children) {
  const childElements = [].concat(...children).reduce((result, child) => {
    if (child !== false && child !== true && child !== null) {
      if (child instanceof Object) {
        result.push(child)
      } else {
        result.push(createElement("text", { textContent: child }))
      }
    }
    return result
  }, [])
  return {
    type,
    props: Object.assign({ children: childElements }, props),
  }
}

思路:

因为有可能为数组又有可能为对象,这样的参数传过来特别影响代码之后的操作,所以我们定义一个方法在这个方法判断这个参数是否是数组,如果是数组就直接返回,如果是对象包裹在一个数组中然后返回出来.

实现: 我们在杂项Misc这个目录里面创建一个文件夹Arrified文件夹,在这个文件夹想创建index.js这里去实现将children对象转化为数组

const arrified = arg => Array.isArray(arg) ? arg : [arg]

export default arrified

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

image.png

接下来将arrifiedChildren数组中VitrualDOM转化为Fiber对象。我们需要一个循环去将数组中的VirtualDOM构建出一个fiber对象

const reconcileChildren = (fiber, children) => {
  /**
   * children 可能是对象,也有可能是数组
   * 将children转换成数组
   */
  const arrifiedChildren = arrified(children);
  let index = 0;
  let 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",
      stateName: null,
      parent: fiber
    }
    fiber.child = newFiber
    index++
  }
}

这时候就完成了每个DOM对象构建成Fiber的工作,type和props直接从element获取,我们现在还没处理组件节点,根节点只有一个之前已经生成,说明现在就是普通节点tag就是”host_component“, effects我们还是先定义为空数组,后面用到。effectTag是操作的标识而已成为”placement“,stateNode就是当前节点DOM对象,等下我们来完善这个值暂且定义为null。到这里差不多结束了一个子节点的初步构建,但是还是要添加节点关系属性,谁是谁的父级,谁是谁的子集,很明显当前传进来的fiber就是这个节点fiber对象的父级。这时候我们还要为fiber添加子级(child)值为newFiber。

到这里还有一些小问题,如果子节点有多个,第一个子节点才是父节点的子节点,其他子节点都是彼此都是兄弟节点,第二个是第一个兄弟节点,但三个是第二个兄弟节点。所以是第一个子节点才去设置子节点child属性,其他的都需要设置兄弟节点

const reconcileChildren = (fiber, children) => {
  /**
   * children 可能是对象,也有可能是数组
   * 将children转换成数组
   */
  const arrifiedChildren = arrified(children);
  let index = 0;
  let numberOfElements = arrifiedChildren.length;
  let element = null;
  let newFiber = null
  let prevFiber = null

  while(index < numberOfElements) {
    element = arrifiedChildren[index];
    newFiber = {
      type: element.type,
      ···
    }

    // 为0是子节点,其他为彼此的兄弟节点
    if(index === 0) {
      fiber.child = newFiber;
    } else {
      prevFiber.sibling = newFiber
    }

    // 处理完之后最后将前一个fiber对象存储起来
    prevFiber = newFiber;
    
    index++

2. 设置stateNode属性

stateNode属性我们使用createStateNode函数调用去获取。需要参数是当前节点fiber对象newFiber。createStateNode函数里当判断fiber对象tag为”host_component“时,则去生成DOM元素对象。如果是组件的话就应该存储的是组件实例对象。 createDOMElement去生成对应的普通DOM元素对象。

// react/reconciliation/index.js
 newFiber.stateNode = createStateNode(newFiber)
 
 // src/react/Misc/createStateNode/index.js
 import { createDOMElement } from '../../DOM'

const createStateNode = fiber => {
  if(fiber.tag === "host_component") {
    return createDOMElement(fiber)
  }
}

export default createStateNode

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

// src/react/DOM/createDOMElement.js
import updateNodeElement from "./updateNodeElement"

export default 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
}

updateNodeElement为元素节点的更新操作,这里就不贴代码,这个和tinyreact一样,查看详情可以github/tinyReact查看或者翻到前面tinyreact文章也可以查看。

image.png

图中可以看到stateNode已经生成。

3. 设置tag属性

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

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

export default getTag

4. 构建左侧节点数中的剩余子节点Fiber对象

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

按照前面构建节点的顺序,应该找到第二层最左侧的子节点开始构建fiber对象,这个节点我们应该怎么去构建呢?在代码当中,我们使用最小规律查找子级原则,找到一个节点(我们可以以根节点为例),查看他是否有子级,如果有就将这个子级和这个子级的children对象传递给 reconcileChildren(fiber, fiber.props.children)进行创建fiber对象。以此类推,将这个子级的子级作为父级,他的children传递给reconcileChildren继续创建。

const executeTask = fiber => {
  reconcileChildren(fiber, fiber.props.children)
  // 判断当前fiber.child是否有值,有值则返回fiber.child
  if(fiber.child) {
    return fiber.child
  }
  console.log(fiber)
}

const workLoop = deadline => {
  ···

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

虽然只是加了一个判断 返回了fiber.child,这个代码执行还是蛮不好理解的。当执行完reconcileChildren,fiber.child有值的话,executeTask返回一个新的fiber对象,workLoop方法里subTask重新被赋值继续执行executeTask,直到executeTask子节点为空,结束循环。这样左侧子节点树DOM构建Fiber对象。

image.png

5. 构建剩余子节点的fiber对象

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

// 多添加一个p节点
import React, {render} from "./react"

const root = document.getElementById("root")

const jsx = (<div>
  <p>Hello React</p>
  <p>我是同级子节点</p>
</div>)

console.log(jsx)

render(jsx, root)


let currentExecutedFiber = fiber
  while(currentExecutedFiber.parent) {
    // 有同级返回同级
    if(currentExecutedFiber.sibling){
      return currentExecutedFiber.sibling
    }
    // 退到父级
    currentExecutedFiber = currentExecutedFiber.parent
  }

image.png

这样我们就完成了所有子节点的fiber对象的构建!!!

总结

今天完成了根节点和所有子节点的fiber对象的构建,获取根节点的vittualDOM之后根据之前提供fiber对象模型去创建根fiber对象。接着创建子级的fiber对象,第一层子级直接第一个参数传fiber,第二个传递fiber.props.children创建当前子节点对象。后面的子节点继续之前逻辑去生成fiber对象,当子级属性有值,则返回friber.child. 创建完左侧子节点之后,代码运行轨迹到了最后一个子节点,如果这个子节点有兄弟节点,则生成该节点fiber,知道同级子节点为空。继续退回上级执行此操作完成所有fiber对象的创建

好了!!!今天到此为止,下一节我们学习构建effects数组,进入fiber第二阶段commit,实现初始化渲染,敬请期待!!!