React学习第十天---React & Fiber(构建Fiber对象第二阶段)(四)

504 阅读15分钟

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

项目代码, 大家可以持续跟进学习,共勉!!!

前言

大家好我是小村儿,前面一篇文章我们已经完成了virtualDOM对象的Fiber对象的构建,接下来我们完成Fiber算法第二阶段

  • 构建effects数组
  • 实现初始化渲染
  • 处理类组件
  • 处理函数组件
  • 实现更新节点
  • 实现节点删除操作
  • 实现类组件更新状态功能

构建effects数组

接下来我们要做的就是将所有的Fiber对象存储在数组中,那为什么要存在数组中呢?

因为在Fiber算法的第二阶段,我们要循环这个数组统一获取fiber对象从而构建真实DOM对象,并且要将构建起来的真实DOM对象挂载在页面当中。我们怎么构建这个数组呢?这就会依赖每个fiber对象里面的effects数组了。

effects数组就是用来存放fiber对象的,我们的目标是将所有fiber对像存储在最外层节点的effects数组中。怎样才能达到这个目标呢?

我们知道,所有fiber对象都有effects数组,最外层fiber对象的effects数组存放所有的fiber对象,其他effects数组负责协助收集fiber对象,最终我们将所有收集的fiber对象汇总收集到最外层fiber对象的effects数组中。

思路:

当左侧节点树种的节点全部构建完成后以后,我们开启了一个while循环去构建其他节点过程时,我们会找到每一个节点父级fiber对象,这样我们就可以为每一个节点effects数组添加fiber对象了。准确来说我们要通过数组的合并操作父级effects数组子级的effects数组的值进行合并,子级effects数组要和子级fiber进行合并,这个数组合并操作在while循环当中不断进行,在循环结束之后,最外层effects数组中就包含所有的fiber对象了,因为循环过程就是不断收集fiber对象的过程。

代码实现:

const executeTask = fiber => {
  ...

  let currentExecutedFiber = fiber
  while(currentExecutedFiber.parent) {
    // 有同级返回同级
    currentExecutedFiber.parent.effects = currentExecutedFiber.parent.effects.concat(
      currentExecutedFiber.effects.concat([currentExecutedFiber])
    )
    ...
  }
  console.log(currentExecutedFiber)
}

image.png

到这里完成了effects数组的构建,且最外层effects包含所有fiber对象!!!

实现初始化渲染

我们只要获得最外层的fiber对象,我们就可以获得包含所有的fiber对象的effects数组了。 看上面的代码我们知道currentExecutedFiber这个变量存储的就是最外层的那个fiber对象

image.png

在fiber第二个阶段当中我们要做真实dom操作,我们要去构建dom节点之间的关系,在构建完成后我们要把真实dom添加到页面当中。

这个需求怎么去实现呢?

  • 第一步:将currentExecutedFiber提升为全局变量 因为在调用第二阶段渲染函数的时候,需要currentExecutedFiber这个变量传递给第二阶段的方法。只有传递给这个方法才会获取effects数组,才能循环effects数组。拿到fiber对象队形进行真实dom操作
// 等待被提交
let pendingCommit = null

const commitAllWork = fiber => {
  console.log(fiber.effects)
}

const executeTask = fiber => {
  ...
  let currentExecutedFiber = fiber
  while(currentExecutedFiber.parent) {
    // 有同级返回同级
    ...
  }
  pendingCommit = currentExecutedFiber
  console.log(currentExecutedFiber)
}

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

image.png

  • 第二步: 将currentExecutedFiber在commitAllWork 遍历,操作fiber对象

这样可以看出上面JSX的输出的Fiber对象,第一个是文本节点,所以我们是倒序获取fiber对象的,也就是从左侧最后一个子节点开始收集的。

我们先确定每个fiber对象的effectTag是什么类型在对齐进行相应的操作,如果是placement则进行节点追加操作.

const commitAllWork = fiber => {
    fiber.effects.forEach((item) => {
        if(item.effectTag === "placement") {
          item.parent.stateNode.appendChild(item.stateNode)
        }
   })
}

image.png

就此我们完成了初始化渲染,完成了一个最最最初级的fiber算法。

类组件处理

1. 先准备好一个类组件

我们先准备好一个类组件,类组件需要继承Component类,这个类在tinyReact也实现过先搬过来使用。新建一个文件夹Component同样在创建一个index.js文件在这个文件里面创一个Component类

// src/react/Component/index.js

export class Component {
  constructor(props) {
    this.props = props;
  }
}


class Creating extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    return <div>hahhahaha</div>
  }
}

render(<Creating/>, root)

image.png

2. 构建组件tag属性

如图知道这样拿到的fiber对象的tag为undefined,因为在getTag方法中我们只处理了type为string的情况,那我们接下来处理组件的情况

import { Component } from "../../Component"
const getTag = vdom => {
  if(typeof vdom.type === 'string') {
    return "host_component"
  } else if(Object.getPrototypeOf(vdom.type) === Component) {
    return "class_component"
  } else {
    return "function_component"
  }
}

export default getTag

image.png

3. 构建组件stateNode属性

我们看图。stateNode属性还是undefined的,前面讲过如果这个DOM节点时一个普通的元素则存储的是一个普通的DOM实例对象,如果是组件的话则存储的是组件实例对象。所以接下来我们要对createStateNode进行加工,处理组件这种情况.

我们在Misc文件夹中创建一个createReactInstance文件夹在这里面index.js 文件中创建一个方法createReactInstance来处理组件返回组件实例对象,一共处理两种组件函数和类组件

// src/react/Misc/createReactInstance/index.js
export const createReactInstance = fiber => {
  let instance = null;
  if(fiber.tag === "class_component") {
    instance = new fiber.type(fiber.props)
  } else {
    instance = fiber.type
  }
  return instance
}
// src/react/Misc/createStateNode/index.js
import { createDOMElement } from '../../DOM'
import {createReactInstance} from "../createReactInstance"

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

export default createStateNode

image.png

这样就完成了组件的stateNode的构建

4. 优化executeTask方法,处理组件添加fiber任务

如果是组件,他的子级就不是直接fiber.props.children了,而是类组件里面render方法返回的内容,所以当调用reconcileChildren方法的时候需要判断是组件还是普通元素,是组件的话调用render方法(也就是组件实例的renderfiber.stateNode.render())在传递给reconcileChildren方法才行,

const executeTask = fiber => {
  if(fiber.tag === "class_component") {
    reconcileChildren(fiber, fiber.stateNode.render())
  } else {
    reconcileChildren(fiber, fiber.props.children)
  }
  
  ...
  }

image.png

这样就拿到组件的子元素。

5. 在commitAllwork处理,将类组件DOM渲染到页面

因为类组件本身也是一个节点,但是类组件本身的节点是不可以追加元素的。所以我们要往组件父级查找,直到找到父级不是组件是普通元素就开始做元素追加操作

const commitAllWork = fiber => {
  fiber.effects.forEach((item) => {
    if(item.effectTag === "placement") {
      let fiber = item
      let parentFiber = item.parent
      // 循环遍历父级tag是否是组件,如果是再往上查找
      while(parentFiber.tag === "class_component") {
        parentFiber = parentFiber.parent
      }
      // 为普通元素就追加到页面中
      if(fiber.tag === "host_component") {
        parentFiber.stateNode.appendChild(item.stateNode)
      }
    }
  })
}

我们成功将组件内容渲染到页面中了!!! image.png

函数组件的处理

准备工作,创建一个函数组件

function FnComponent() { 
  return <div>FnComponent</div>
}

render(<FnComponent/>, root)

优化executeTask方法,处理函数组件任务

我们知道函数组件的stateNode存储的是函数组件的函数,调用函数组件即可获取函数组件的内容

const executeTask = fiber => {
  if(fiber.tag === "class_component") {
    reconcileChildren(fiber, fiber.stateNode.render())
  } else if(fiber.tag === "function_component") {
    reconcileChildren(fiber, fiber.stateNode(fiber.props))
  }else {
    reconcileChildren(fiber, fiber.props.children)
  }

优化commitAllWork方法,处理函数组件内容渲染到页面

和处理类组件一样如果是函数组件也一样一直往上查找直到不是组件为止完成页面的元素追加

const commitAllWork = fiber => {
  fiber.effects.forEach((item) => {
    if(item.effectTag === "placement") {
      let fiber = item
      let parentFiber = item.parent
      while(parentFiber.tag === "class_component" || parentFiber.tag === "function_component") {
        parentFiber = parentFiber.parent
      }
      if(fiber.tag === "host_component") {
        parentFiber.stateNode.appendChild(item.stateNode)
      }
    }
  })
}

image.png 函数组件渲染成功,且类组件和函数组件添加属性也可以得到!!!

实现更新节点 oldFiber VS newFiber

准备一段测试代码

这两个JSX只是从Hello React替换成 奥利给操作,当dom初始化完成渲染之后,我们去备份旧的fiber对象,在两秒钟之后呢我们又调用了render方法,就会又去创建节点fiber对象了,创建fiber对象的时候我们会看一下就得fiber节点存不存在,如果旧的fiber对象存在则说明当前我们要执行更新操作。这是后我们就去创建更新fiber节点对象。

const jsx = (<div>
  <p>Hello React</p>
  <p>Hello FIber</p>
</div>)
render(jsx, root)

setTimeout(() => {
  const jsx = (<div>
    <p>奥利给</p>
    <p>Hello FIber</p>
  </div>)
  render(jsx, root)
}, 2000);

优化commitAllWork方法,添加备份旧Fiber对象逻辑

const commitAllWork = fiber => {
  fiber.effects.forEach((item) => {
    ···
    
  })
  /***
     * 备份旧的fiber节点对象***/
    fiber.stateNode.__rootFiberContainer = fiber
}

优化getFirstTask方法,添加alterNate标记

在创建新fiber对象的时候,将旧fiber对象存储到alterNate中,在构建更新fiber对象的时候用到。

const getFirstTask = () => {
 ···
  /**
   * 返回最外层节点的fiber对象
   */
  return {
    ···
    alternate: task.dom.__rootFiberContainer
  }
}

优化reconcileChildren方法

在这个方法里我们要判断fiber对象需要进行什么样的操作,去构建不同操作类型的fiber对象

  • 第一步: 先获取备份节点

reconcileChildren这个方法是在构建子节点,按道理来说我们要获取对应的子节点的fiber对象,如何获取呢?

思路:

定义一个alternate变量,接收备份节点 如果fiber.alternate有值则说明有备份节点,则获取备份节点的子节点,fiber.alternate.child表示的是第一个子节点,如果都有值将fiber.alternate.child赋值给alternate

代码实现:

let alternate = null;

if(fiber.alternate && fiber.alternate.child) {
    alternate = fiber.alternate.child;
}

fiber.alternate.child找到这个也就是找到备份子节点,这个节点时哪一个节点的备份节点实际上就是children数组中的第一个节点的子节点

  • 第二步 更新各个子节点的alternate 当方法里面不循环不断执行的时候,第一次循环就是第一个子节点,第二次是找到第二个子节点,第三个...

接下来就更新子节点的alternate,要不然alternate指向的就一直是第一个子节点的备份节点了

if(alternate && alternate.sibling) {
  alternate = alternate.sibling
} else {
// 走到这里说明同级子节点都已经找完
  alternate = null
}

所以在reconcileChildren方法内alternate就是旧节点,element就是新节点

  • 第三步:确定操作类型 我们是做初始渲染还是做更新还是删除呢?根据什么判断

如果element存在alternate不存在,做初始渲染操作(这个之前已经做过不改变) 如果element存在alternate存在,做更新操作, 将effectTag改成update,并且添加alternate属性。 - element和alternate当类型相同的时候,替换newFiber.stateNode的值为alternate.stateNode - element和alternate当类型(type)不相同相同的时候,newFiber.stateNode值为createStateNode(newFiber)

const reconcileChildren = (fiber, children) => {
  ···

  while (index < numberOfElements) {
    element = arrifiedChildren[index];
    if (element && alternate) {
      /**
       * 更新
       */
      newFiber = {
        type: element.type,
        props: element.props,
        tag: getTag(element),
        effects: [],
        effectTag: "update",
        stateNode: null,
        parent: fiber,
        alternate,
      };
      if (element.type === alternate.type) {
        /**
         * 类型相同
         */
        newFiber.stateNode = alternate.stateNode;
      } else {
        /**
         * 类型不同
         */
        newFiber.stateNode = createStateNode(newFiber);
      }

      newFiber.stateNode = createStateNode(newFiber);
    } else if (element && !alternate) {
      /**
       * 初始化渲染
       */
      newFiber = {
        type: element.type,
        props: element.props,
        tag: getTag(element),
        effects: [],
        effectTag: "placement",
        stateNode: null,
        parent: fiber,
      };

      newFiber.stateNode = createStateNode(newFiber);
    }

    ···
  }
};

  • 第四步: 执行DOM操作

这时候可以在commitAllWork中判断efftctTag是update时对dom进行更新,类型相同和不同的使用不同操作更新dom

if(item.effectTag === "update") {
      /**
       * 更新
       */
      if(item.type === item.alternate.type) {
        /**
         * 节点类型相同
         */

        updateNodeElement(item.stateNode, item, item.alternate)
      } else {
        /**
         * 节点类型不同
         */
        item.parent.stateNode.replaceChild(item.stateNode, item.alternate.stateNode)
      }
    }

第五步:优化类型相同时处理更新节点方法 但是在类型相同的时候不止处理普通元素节点还有文本节点,但是当前updateNodeElement只处理普通元素节点,还不能处理文本节点,我们接下来对updateNodeElement函数进行优化

if(virtualDOM.type === "text") {
    if(newProps.textContent !== oldProps.textContent) {
      if(virtualDOM.parent.type !== oldVirtualDOM.parent.type) {
        virtualDOM.parent.stateNode.appendChild(document.createTextNode(newProps.textContent))
      } else {
        virtualDOM.parent.stateNode.replaceChild(
          document.createTextNode(newProps.textContent),
          oldVirtualDOM.stateNode
        )
      }
      
    }
    return
  }

image.png image.png

完成dom更新

实现节点删除操作

1. 准备工作

还是准备两段jsx,两秒钟过后删除一个p标签

const jsx = (<div>
  <p>Hello React</p>
  <p>Hello FIber</p>
</div>)
render(jsx, root)

setTimeout(() => {
  const jsx = (<div>
    <p>Hello FIber</p>
  </div>)
  render(jsx, root)
}, 2000);

2. 优化reconcileChildren方法,完成删除代码

如果element不存在且alternate存在,则说明需要进行删除操作.注意执行删除操作的时候arrifiedChildren数组有可能空数组,循环就进不来了需要修改一下

这里需要优化的一些地方,需要改善,element不存在 reconcileChildren 循环判断 需要判断alternate也应该进入循环

element存在的时候 才会添加新节点

// 多加判断
  while (index < numberOfElements || alternate) {
    element = arrifiedChildren[index];
    
 // 添加element
if (index === 0) {
  fiber.child = newFiber;
} else if(element){
  prevFiber.sibling = newFiber;
}
if(!element && alternate){
      /**
       * 删除操作
       */
      alternate.effectTag = "delete"
      fiber.effects.push(alternate);
    }

3. 优化commitAllWork,完成删除DOM操作

const commitAllWork = (fiber) => {
  fiber.effects.forEach((item) => {
    if(item.effectTag === "delete") {
      item.parent.stateNode.removeChild(item.stateNode)
    }
  });
};

image.png

image.png

完成删除

实现类组件状态更新功能

1. 准备工作

准备一个类组件,设置一个state然后设置一个name对象

class Creating extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '张三',
    }
  }
  render() {
    return <div>
      {this.props.title}hahhahaha{this.state.name}
      <button onClick={() => this.setState({name: "李四"})}>button</button>
      </div>
  }
}
render(<Creating title={"hello"}/>, root)

image.png

2. 分析实现组件状态更新

当组件状态发生更新的时候我们要把它当做一个任务,放到一个任务队列当中。还要指定当浏览器空闲时执行该任务队列。在执行任务的时候我们要将状态更新任务和其他任务进行区分,所以在添加任务时在任务对象里面添加一个标识。在任务对象当中我们还需要添加一个实例对象和即将要更新的组件状态对象。因为我们要在组件实例对象当中获取原本是的组件状态对象。因为只有获得了原本的组件状态对象,和即将要更新的组件状态对象我们才能更新这个state对象。

还有一个问题,如何将state对象当中的数据更新到真实DOM对象当中呢?我们从根节点开始为每个节点重新构建fiber对象,从而创建出执行更新操作的fiber对象,在进行fiber第二个阶段时,就可以将更新应用到正式dom当中了。

既然说我们要从根节点构建fiber对象,怎样才能获得已经生成的fiber对象呢?因为组件在发生更新的时候,这个根节点的fiber对象就已经存在了。我们要根据这个已经存在的fiber对象构建新的根节点fiber对象。

我们可以将组件的fiber对象备份到组件的实例对象身上。因为在组件状态发生更新时,是可以获取到组件实例对象的(stateNode),通过组件的实例对象获取到组件的fiber对象,通过组件的fiber对象就可以向上一层一层查找了,最终我们就可以获得最外层fiber对象了。

3. 代码实现

  • 实现setState

该方法一个形参,为partialState更新的状态,setState方法里面调用scheduleUpdate方法传递两个值第一个this代表当前组件实例对象,第二个我状态更新的值 partialState

import {scheduleUpdate} from "../reconciliation"
export class Component {
  constructor(props) {
    this.props = props;
  }
  setState(partialState) {
    scheduleUpdate(this, partialState);
  }
}

  • 实现scheduleUpdate 上面说了,该方法接收两个参数,第一个instance(组件实例),第二个参数是partialState。怎么完成更新呢,我们把partialState内容覆盖组件实例中的state内容即可。我们将组件更新当做一个任务将这个任务放入组件队列当中。
export const scheduleUpdate = (instance, partialState) => {
  taskQueue.push({
    from: "class_component",
    instance,
    partialState
  })
  requestIdleCallback(performTask)
}

当组件已经渲染到页面时候,任务队列这时候是空的,点击按钮时将组件状态更新存入任务队列中,则任务队列只有一个任务,所以会去执行getFirstTask获取到state更新任务,所以在这个getFirstTask对任务进行处理.在这个方法里面我们要去更新最外层的fiber对象,我们更具以前存在的fiber对象进行构建。

那怎么找到之前的最外层的fiber对象呢,我们在commitAllWork的时候讲fiber对象暂存在stateNode属性当中(当时类组件的时候)

 if(item.tag === "class_component") {
      item.stateNode.__fiber = item
    }

设置完值之后在Misc中创建一个getRoot的方法循环获取到最外层fiber对象


const getRoot = instance => {
  let fiber = instance.__fiber;
  while(fiber.parent) {
    fiber = fiber.parent
  }
  return fiber
}

export default getRoot

image.png

将这个方法在getFirst里面调用并返回一个更新后fiber实例

const getFirstTask = () => {
  /**
   * 从任务队列中获取任务
   */
  const task = taskQueue.pop();

  if(task.from === "class_component") {
    const root = getRoot(task.instance)
    task.instance.__fiber.partialState = task.partialState
    return {
      props: root.props,
      stateNode: root.stateNode,
      tag: "host_root",
      effects: [],
      child: null,
      alternate: root
    }
  }
  ···

获取到任务之后我们去executeTask执行任务,将任务里面的状态进行更新并放入fiber的stateNode的state中

const executeTask = (fiber) => {
  if (fiber.tag === "class_component") {
    if(fiber.stateNode.__fiber && fiber.stateNode.__fiber.partialState) {
      fiber.stateNode.state = {
        ...fiber.stateNode.state,
        ...fiber.stateNode.__fiber.partialState
      }
    }
    ···

image.png 点击按钮完成更新!!!

image.png

总结

今天我们学习了Fiber的第二阶段,fiber算法的理解告一段落,提交Fiber对象,完成DOM添加到页面当中。

首先我们会去创建effects数组,每个节点会有一个effects数组,会合并自己节点本身fiber对象和effects合并,在将这个effects数组合并给父级effects数组,最后所有fiber对象都会在最外层fiber对象中。

然后遍历这个最外层fiber对象的effects数组,将子级的stateNode添加到父级stateNode中完成了DOM树构建,最后将DOM添加到页面中。这里完成了一个最最最简单的fiebr算法。 后面我们完成了函数组件和类组件的的处理,结合前面最基本的fiber算法根据不同类型组件进行方法优化,例如类组件的子元素是render方法里面的一部分而不是stateNode,例如函数组件的stateNode是一个方法,接收一个type生成DOM对象。类组件最外层忽略因为组件标签不需要渲染至页面,...不同组件完成tag和stateNode等属性优化即可。

最后我们要完成组件更新,这个就需要备份之前的fiber对象,才能更好的完成oldFiber VS newFiber 的比对。在提交Fiber对象完成DOM构建的时候,将最外层fiber.stateNode.__rootFiberContainer = fiber对象进行备份,然后下次更新之前的时候讲旧的最外层子节点放入如下alternate当中,处理任务的时候通过alternate将所有节点的旧fiber提取出来。element就是新fiebr,alternate提取出来的就是旧fiber。element有alternate有就是更新操作。element没有alternate有就是删除操作。这样完成比对之后完成dom更新,dom删除。之后还有一个类组件state更新完成dom更新,我们是将state更新也当做一个特殊任务。在点击按钮的时候状态改变这个任务添加到任务队列当中,添加进去之后给一个state更新标记。然后根据新的任务更新一个新的根部fiber对象,最后将组件实例中的state通过partialState进行状态更新,改变了组件state完成dom更新。

我们完成了整个FIber算法,希望大家根据文章对fiber有一个系统的了解,谢谢大家的支持,欢迎点赞评论关注,大家一起进步 项目代码, 大家可以持续跟进学习,共勉!!!