Skip to content

构建Fiber对象第二阶段

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

  • 因为 Fiber 算法的第二阶段,我们要循环这个数组统一获取 fiber 对象从而构建真实 DOM 对象,并且要将构建起来的真实 DOM 挂在到页面当中

我们要怎么构建这个数组呢?

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

构建 Effect 数组

思路:

  • 当左侧节点树种的节点全部构建完成以后,我们开启一个 while 循环去构建其他节点过程时,我们会找到每一个节点父级 fiber 对象,这样我们就可以为每一个节点 effects 数组添加 fiber 对象了

image-20230118180235464

js
const executeTask = fiber => {
  console.log(fiber)
  let currentExecuteFiber = fiber
  while (currentExecuteFiber.parent) {
    currentExecuteFiber.parent.effects = currentExecuteFiber.parent.effects.concat(
      currentExecuteFiber.effects.concat([currentExecuteFiber])
    )
  }
  console.log(currentExecuteFiber)
}

image-20230119094021968

实现初始化渲染

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

  1. 将 currentExecutedFiber 提升为全局变量 因为在调用第二阶段渲染函数的时候,需要 currentExecutedFiber 这个变量传递给第二阶段的方法

    jsx
    let pendingCommit = null
    
    const commitAllWork = fiber => {
      console.log(fiber.effects)
    }
    
    const executeTask = fiber => {
      // ...
      pendingCommit = currentExecuteFiber
    }
    
    const workLoop = deadline => {
    	// ...
      if (pendingCommit) {
        commitAllWork(pendingCommit)
      }
    }
    
    const jsx = (<div>
      <p>Hello React</p>
      <p>我是同级子节点</p>
    </div>)
  2. 将 currentExecutedFiber 在 commitAllWork 遍历,操作 fiber 对象

    image-20230119105238478

    第一个是文本节点,所以我们是倒序获取 fiber 对象的,也就是从左侧最后一个子节点开始收集的,我们先确定每个 fiber 对象的 effectTag 是什么类型再对其进行相应的操作,如果是 placement 则进行节点追加操作

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

类组件处理

准备类组件

首先我们准备好一个类型组件,类组件需要继承 Component 类,并新建一个文件夹 Component 在里面创建 index.js 存放 Component 类

jsx
import React, { render, Component } from './react'
class Greeting extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    return <p>Hello React</p>
  }
}
render(<Greeting />, root)

// Component/index.js
export class Component {
  constructor(props) {
    this.props = props
  }
}

构建组件 tag 属性

这样处理后,打印 newFiber,得到的 tag 是 undefined,因为 getTag 方法中我们只处理了 type -> string 的情况

image-20230119144909625

js
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'
  }
}

image-20230119144945669

构建组件 stateNode 属性

stateNode 还是 undefined,如果这个 DOM 节点是一个普通的元素则存储的是一个普通的 DOM 实例对象,一共处理两种组件:函数、类组件

js
// 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
}

// 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)
  }
}

image-20230119161434214

优化 executeTask 方法

如果是组件,他的子级就不是直接 fiber.props.children 了,而是类组件里面 render 方法返回的内容,所以调用 reconcileChildren 方法的时候需要帕努单是组件还是普通元素,是组件的话调用 render 方法

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

这样就拿到组件子元素了

image-20230119162010307

commitAllWork 处理

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

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

函数组件处理

准备一个函数式组件

js
function FnComponent(){
  return <div>FnComponent</div>
}
render(<FnComponent />, root)

image-20230119162806760

executeTask 处理函数组件

js
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)
  }
}

处理函数组件内容渲染到页面

js
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(fiber.stateNode)
      }
      item.parent.stateNode.appendChild(item.stateNode)
    }
  })
}

更新节点 oldFiber/newFiber

这两个JSX只是从 Hello React 替换成 奥利给 操作,当 dom 初始化完成渲染之后,我们去备份旧的 fiber 对象,在两秒钟之后我们又调用了 render 方法,就会去创建新的 fiber 对象了

创建 fiber 对象的时候我们会看一下 fiber 节点存不存在,如果旧的 fiber 对象存在则说明当前我们要执行更新操作

js
const jsx = (
  <div>
    <p>Hello React</p>
    <p>Hi Fiber</p>
  </div>
)
render(jsx, root)

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

commitAllWork 方法,执行的就是 dom 操作

js
const commitAllWork = fiber => {
  // ...
  // 备份旧的 fiber 节点对象
  fiber.stateNode.__rootFiberContainer = fiber
}

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

js
const getFirstTask = () => {
  // ...
  return {
    // ...
    alternate: task.dom.__rootFiberContainer
  }
}

reconcileChildren

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

  1. 先获取备份节点

    定义一个 alternate 变量,接收备份节点,如果 fiber.alternate 有值则说明有备份节点,则获取备份节点的子节点

    如果都有值将 fiber.alternate.child 赋值给 alternate

    js
    let alternate = null
    
    if (fiber.alternate && fiber.alternate.child) {
    	alternate = fiber.alternate.child
    }
  2. 更新各个子节点的 alternate,当方法里面不循环不执行的时候,第一次循环就是第一个子节点,第二次是找到第二个子节点...

    js
    if (alternate && alternate.sibling) {
      alternate = alternate.sibling
    } else {
      alternate = null
    }
  3. 确定操作类型,我们是做初渲染?还是更新?还是删除?

    如果 element 存在,alternate 不存在,做的是初始渲染操作

    如果 element 存在,alternate 存在,做的是更新操作

    js
    const reconcileChildren = (fiber, children) => {
      // ...
      while (index < numberOfElements) {
        /**
         * 子级 virtualDOM 对象
         */
        element = arrifiedChildren[index]
    
        if (element && alternate) {
          // 更新操作
          newFiber = {
            type: element.type,
            props: element.props,
            tag: getTag(element),
            effects: [],
            effectTag: 'update',
            parent: fiber,
            alternate
          }
          if (element.type === alternate.type) {
            // 类型相同
            newFiber.stateNode = alternate.stateNode
          } else {
            // 类型不同
            newFiber.stateNode = createStateNode(newFiber)
          }
        } else if (element && !alternate) {
          // 初始渲染
          /**
           * 子级 fiber 对象
           */
          newFiber = {
            type: element.type,
            props: element.props,
            tag: getTag(element),
            effects: [],
            effectTag: 'placement',
            parent: fiber
          }
    
          /**
           * 为 fiber 节点添加 DOM 对象或组件实例对象
           */
          newFiber.stateNode = createStateNode(newFiber)
    
          console.log(newFiber)
    
          // 为父级 fiber 添加子级 fiber
          if (index === 0) {
            fiber.child = newFiber
          } else {
            // 为 fiber 添加下一个兄弟 fiber
            prevFiber.sibling = newFiber
          }
    
          if (alternate && alternate.sibling) {
            alternate = alternate.sibling
          } else {
            alternate = null
          }
    
          prevFiber = newFiber
          index++
        }
      }
    }
  4. 执行 DOM 操作

    在 commitAllWork 中判断 effectTag 是 update 时对 dom 进行更新

    js
    const commitAllWork = fiber => {
      fiber.effects.forEach(item => {
        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)
          }
        }
      })
    }

实现节点删除操作

优化 reconcileChildren 方法

js
const reconcileChildren = (fiber, children) => {
  // ...
  while (index < numberOfElements || alternate) {
    // ...
    if (!element && alternate) {
      // 删除操作
      alternate.effectTag = 'delete'
      fiber.effects.push(alternate)
    }
  }
}

完成删除 DOM 操作

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

常备不懈,才能有备无患