React Fiber 04 - 渲染组件、节点更新、节点删除

渲染类组件

示例

// src/index.js
import React, { render, Component } from './react'

const root = document.getElementById('root')

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

class Greating extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    return <div>Hello Class Component</div>
  }
}
render(<Greating />, root)

添加继承类 Component

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

// src/react/index.js
import createElement from './CreateElement'
export { render } from './Reconciliation'
export { Component } from './Component'
export default {
  createElement
}

扩展 getTag

扩展getTag(),支持组件类型:

// src\react\Misc\GetTag\index.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'
  }
}

export default getTag

扩展 createStateNode

扩展createStateNode(),支持组件:

  • 类组件:返回组件实例对象
  • 函数组件:返回定义组件的方法
// src\react\Misc\CreateStateNode\index.js
import { createDOMElement } from '../../DOM'
import { createReactInstance } from '../CreateReactInstance'

const createStateNode = fiber => {
  if (fiber.tag === 'host_component') {
    // 普通节点返回真实 DOM 对象
    return createDOMElement(fiber)
  } else {
    // 组件节点返回组件实例对象
    return createReactInstance(fiber)
  }
}

export default createStateNode

// src\react\Misc\CreateReactInstance\index.js
/**
 * 获取组件的 StateNode
 * @param {*} fiber
 */
export const createReactInstance = fiber => {
  let instance = null
  if (fiber.tag === 'class_component') {
    // 类组件
    instance = new fiber.type(fiber.props)
  } else {
    // 函数组件
    instance = fiber.type
  }
  return instance
}

获取组件的 children

React 在运行之前 Babel 会将 JSX 转换成 React.createElement() 的调用。

如果 JSX 是普通元素,则会将子元素传递给 React.createElement()

const jsx = (
  <div>
    <p>Hello React</p>
    <p>Hi Fiber</p>
  </div>
)

转换为:

const jsx = /*#__PURE__*/ React.createElement(
  "div",
  null,
  /*#__PURE__*/ React.createElement("p", null, "Hello React"),
  /*#__PURE__*/ React.createElement("p", null, "Hi Fiber")
);

但如果是组件:

class ClassComponent extends React.Component {
  render() {
    return <div>Hi Class</div>
  }
}
const jsx_class = <ClassComponent />

function FunctionComponent() {
  return <div>Hi Fcuntion</div>
}
const jsx_function = <FunctionComponent />

则直接将组件传递进去,组件的子级(即组件返回的 JSX 内容)需要通过调用组件的方法获取:

  • 类组件调用render()方法
  • 函数组件调用自身
class ClassComponent extends React.Component {
  render() {
    return /*#__PURE__*/ React.createElement("div", null, "Hi Class");
  }
}

const jsx_class = /*#__PURE__*/ React.createElement(ClassComponent, null);

function FunctionComponent() {
  return /*#__PURE__*/ React.createElement("div", null, "Hi Fcuntion");
}

const jsx_function = /*#__PURE__*/ React.createElement(FunctionComponent, null);

executeTask() 中向构建子级 Fiber 对象的方法 reconcileChildren 传递参数的时候,之前仅处理了普通节点。

const reconcileChildren = (fiber, children) => {
  console.log(children);
  /*...*/
}
const executeTask = fiber => {
  // 构建子级 fiber 对象
  reconcileChildren(fiber, fiber.props.children)
  /*...*/
}

所以当前传递的组件的 fiber.props.children 为空 []

在这里插入图片描述

需要判断,当为组件节点的时候调用方法获取 children

const executeTask = fiber => {
  // 构建子级 fiber 对象
  if (fiber.tag === 'class_component') {
    reconcileChildren(fiber, fiber.stateNode.render())
  } else {
    reconcileChildren(fiber, fiber.props.children)
  }
  /*...*/
}

在这里插入图片描述

组件节点本身也是一个节点。

构建组件的 Fiber 节点,组件的子级是组件返回的 JSX 内容,而不是 JSX 内容的子级。

第二阶段-追加节点

现在类组件的 Fiber 对象渲染完成,进入第二阶段。

我们对组件和组件返回的 JSX 的节点都构建了 Fiber 对象,组件节点本身也是一个节点,但是组件节点本身不能作为真实的 DOM 元素去操作:

  • 被追加到页面中
  • 追加真实 DOM 元素

所以要递归查找组件节点的普通节点父级(组件可能被包含在另一个组件中,所以要向上递归查找),这样才能去操作 DOM 元素的追加。

并且在追加时判断,仅当节点是普通节点类型时,执行追加操作。

const commitAllWork = fiber => {
  fiber.effects.forEach(item => {
    if (item.effectTag === 'placement') {
      // 当前要追加的子节点的父级
      let parentFiber = item.parent
      /**
       * 找到普通节点父级 排除组件父级
       * 因为组件父级是不能直接追加真实 DOM 节点的
       */
      while (parentFiber.tag === 'class_component') {
        parentFiber = parentFiber.parent
      }
      // 如果子节点时普通节点 将子节点追加到父级中
      if (item.tag === 'host_component') {
        parentFiber.stateNode.appendChild(item.stateNode)
      }
    }
  })
}

现在类组件就渲染完成,可以访问页面查看结果。

总结

  1. 设置类组件 Fiber 对象的 tag 属性为 class_component
  2. 设置类组件 Fiber 对象的 stateNode 属性为组件实例对象
  3. 通过调用类组件实例对象的 render() 方法获取组件的子级:组件返回的 JSX
  4. 追加组件内容:
    • 类组件节点不能作为真实 DOM 节点去追加内容和被追加
    • 需要向上循环递归查找它所属的普通节点类型的父级节点
    • 在追加节点时判断,只有普通节点可以被追加到页面

渲染函数组件

示例:

// src/index.js
import React, { render, Component } from './react'

const root = document.getElementById('root')

function FnComponent(props) {
  return <div>{props.title}</div>
}

render(<FnComponent title="Function Component" />, root)

函数组件和类组件几乎一样,区别:

  • tag的不同
    • 类组件:class_component
    • 函数组件:function_component
  • stateNode 不同
    • 类组件:组件实例
    • 函数组件:组件本身(函数方法)
  • 获取子级的方式不同:
    • 类组件通过调用实例对象的 render()方法获取
    • 函数组件通过调用组件本身的方法获取

之前获取 tagstateNode的修改已经支持了函数组件。

获取函数组件的子级:

const executeTask = fiber => {
  // 构建子级 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)
  }
  /*...*/
}

查找普通节点父级增加函数组件的判断:

const commitAllWork = fiber => {
  fiber.effects.forEach(item => {
    if (item.effectTag === 'placement') {
      // 当前要追加的子节点的父级
      let parentFiber = item.parent
      /**
       * 找到普通节点父级 排除组件父级
       * 因为组件父级是不能直接追加真实 DOM 节点的
       */
      while (parentFiber.tag === 'class_component' || parentFiber.tag === 'function_component') {
        parentFiber = parentFiber.parent
      }
      // 如果子节点时普通节点 将子节点追加到父级中
      if (item.tag === 'host_component') {
        parentFiber.stateNode.appendChild(item.stateNode)
      }
    }
  })
}

函数组件渲染完成。

实现节点更新

当前仅处理普通节点的更新

实现思路

  • 当 DOM 初始化渲染完成之后,要备份旧的 Fiber 节点对象。
  • 当再次调用 render() 方法更新 DOM 的时候,又再次创建 FIber 节点对象
  • 当再次创建 Fiber 节点对象的时候要检查是否存在旧的 Fiber 节点对象
    • 如果存在,则表示当前执行的是更新操作
      • 此时就要创建执行更新操作的 Fiber 节点对象
    • 否则就是初始化渲染

示例

// src/index.js
import React, { render, Component } from './react'

const root = document.getElementById('root')

const jsx = (
  <div>
    <p>Hello React</p>
    <p>Hi Fiber</p>
  </div>
)

render(jsx, root)

setTimeout(() => {
  const jsx = (
    <div>
      <p>你好 React</p>
      <p>Hi Fiber</p>
    </div>
  )
  render(jsx, root)
}, 2000)

备份旧的 Fiber 节点对象

初始化渲染完成就是指 DOM 操作完成之后,也就是 commitAllWork() 中的内容执行完成之后。

在该方法中备份旧的 Fiber 节点对象,只需将根节点对应的 Fiber 对象存储到根节点对应的真实 DOM 对象上即可。

// src/react/Reconciliation/index.js
import { createTaskQueue, arrified, createStateNode, getTag } from '../Misc'
/*...*/

// 存储根节点所对应的 Fiber 对象
let pendingCommit = null

const commitAllWork = fiber => {
  // 循环 effects 数组 构建 DOM 节点树
  fiber.effects.forEach(item => {
    /*...*/
  })

  // 备份旧的 Fiber 节点对象
  fiber.stateNode.__rootFiberContainer = fiber
}

/*...*/

根节点的 Fiber 对象中存储备份

Fiber 对象中的 alternate 属性存储旧 Fiber 对象的备份,用于新旧对比。

首先修改构建根节点 Fiber 对象的方法getFirstTask()

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

  // 返回最外层节点的 Fiber 对象
  return {
    props: task.props,
    stateNode: task.dom,
    tag: 'host_root',
    effects: [], // 暂不指定
    child: null, // 在构建子节点的时候指定其与父节点的关系
    alternate: task.dom.__rootFiberContainer // 旧的 Fiber 节点对象
  }
}

子节点的 Fiber 对象中存储备份

然后找到构建子节点 Fiber 对象的方法reconcileChildren()

  • 该方法中会循环构建节点的子节点
  • 在循环之前判断节点是否备份了旧 Fiber 对象
  • 如果有,则获取备份的Fiber对象中的子级(child:存储的是该节点下第一个子节点)
  • 然后进入构建子节点的循环中
  • 构建Fiber对象的时候,判断是否有备份,如果有则将备份存储到 alternate 属性
  • 然后判断该备份的 Fiber 对象中是否有兄弟节点(sibling
  • 如果有,则获取这个兄弟节点,它就是下次循环构建的子节点
const reconcileChildren = (fiber, children) => {
  // children 可能是对象,也可能是数组
  // 将 children 转换成数组
  const arrifiedChildren = arrified(children)

  // 循环 children 使用的索引
  let index = 0
  // children 数组中元素的个数
  let numberOfElements = arrifiedChildren.length
  // 循环过程中的循环项 就是子节点的 virtualDOM 对象
  let element = null
  // 子级 fiber 对象
  let newFiber = null
  // 上一个兄弟 fiber 对象
  let prevFiber = null
  // 循环过程中节点对应的备份 fiber 对象
  let alternate = null

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

  while (index < numberOfElements) {
    // 子级 virtualDOM 对象
    element = arrifiedChildren[index]

    // 子级 fiber 对象
    newFiber = {
      type: element.type,
      props: element.props,
      tag: getTag(element),
      effects: [], // 暂不指定
      effectTag: 'placement',
      parent: fiber,
      alternate
    }

    // 为 fiber 对象添加 DOM 对象或类组件实例对象或函数组件本身
    newFiber.stateNode = createStateNode(newFiber)

    // 指明父子关系、兄弟关系
    if (index === 0) {
      // 父节点的子节点只能是第一个子节点
      fiber.child = newFiber
    } else {
      // 其它的节点作为上一个节点的兄弟节点
      prevFiber.sibling = newFiber
    }

    if (alternate && alternate.sibling) {
    	// 获取下一个节点的备份
      alternate = alternate.sibling
    } else {
      alternate = null
    }

    prevFiber = newFiber

    index++
  }
}

根据操作构建不同的 Fiber 对象

在构建子节点的时候,还要判断当前要执行什么操作,从而构建不同操作所对应的 Fiber 对象:

  • 初始渲染
  • 更新操作
const reconcileChildren = (fiber, children) => {
  // children 可能是对象,也可能是数组
  // 将 children 转换成数组
  const arrifiedChildren = arrified(children)

  // 循环 children 使用的索引
  let index = 0
  // children 数组中元素的个数
  let numberOfElements = arrifiedChildren.length
  // 循环过程中的循环项 就是子节点的 virtualDOM 对象
  let element = null
  // 子级 fiber 对象
  let newFiber = null
  // 上一个兄弟 fiber 对象
  let prevFiber = null
  // 循环过程中节点对应的备份 fiber 对象
  let alternate = null

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

  while (index < numberOfElements) {
    // 子级 virtualDOM 对象
    element = arrifiedChildren[index]

    if (element && alternate) {
      /* 更新操作 */
      // 子级 fiber 对象
      newFiber = {
        type: element.type,
        props: element.props,
        tag: getTag(element),
        effects: [], // 暂不指定
        effectTag: 'update',
        parent: fiber,
        alternate
      }

      // 判断节点类型
      if (element.type === alternate.type) {
        /* 类型相同 */
        // 只需将之前的 stateNode 赋值给新的 fiber 对象即可
        newFiber.stateNode = alternate.stateNode
      } else {
        /* 类型不同 */
        // 为 fiber 对象添加 DOM 对象或类组件实例对象或函数组件本身
        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)
    }

    // 指明父子关系、兄弟关系
    if (index === 0) {
      // 父节点的子节点只能是第一个子节点
      fiber.child = newFiber
    } else {
      // 其它的节点作为上一个节点的兄弟节点
      prevFiber.sibling = newFiber
    }

    if (alternate && alternate.sibling) {
      // 获取下一个节点的备份
      alternate = alternate.sibling
    } else {
      alternate = null
    }

    prevFiber = newFiber

    index++
  }
}

执行 DOM 操作

执行 DOM 操作是在 commitAllWork() 方法中。

  • 通过 Fiber 对象的effectTag属性判断执行的操作
    • update 更新节点
    • placement 追加节点
  • 如果是更新节点,继续判断节点类型是否相同
    • 节点类型不同,直接用新节点替换旧节点(调用父节点 DOM 的replaceChild()
    • 节点类型相同,执行更新操作(调用 updateNodeElement()
      • updateNodeElement() 方法接收的 VirtualDOM 就是 Fiber 对象,主要使用对象的 props 属性
// src/react/Reconciliation/index.js
import { updateNodeElement } from '../DOM'
import { createTaskQueue, arrified, createStateNode, getTag } from '../Misc'

/*...*/

const commitAllWork = fiber => {
  // 循环 effects 数组 构建 DOM 节点树
  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)
      }
    } else if (item.effectTag === 'placement') {
      /* 追加节点 */
      /*...*/
    }
  })

  // 备份旧的 Fiber 节点对象
  fiber.stateNode.__rootFiberContainer = fiber
}

扩展更新节点的方法 - 更新文本节点

当前更新节点的方法 updateNodeElement 是参考之前的《模拟 React》 文章复制来的。

该文中更新文本节点是调用的另一个方法,所以 updateNodeElement()中没有处理文本节点。

现在扩展这个方法,使其即能处理元素节点,也能处理文本节点:

// src\react\DOM\updateNodeElement.js
/**
 * @param {*} newElement 要更新的 DOM 元素对象
 * @param {*} virtualDOM 新的 Virtual DOM 对象
 * @param {*} oldVirtualDOM 旧的 Virtual DOM 对象
 */
export default function updateNodeElement(newElement, virtualDOM = {}, oldVirtualDOM = {}) {
  // 获取节点对应的属性对象
  const newProps = virtualDOM.props
  const oldProps = oldVirtualDOM.props || {}

  // 文本节点更新操作
  if (virtualDOM.type === 'text') {
    if (newProps.textContent !== oldProps.textContent) {
      virtualDOM.parent.stateNode.textContent = newProps.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
  }

  // 属性被修改或添加属性的情况
  Object.keys(newProps).forEach(propName => {/*...*/})

  // 判断属性被删除的情况
  Object.keys(oldProps).forEach(propName => {/*...*/})
}

总结

  1. 在构建 Fiber 对象的时候要备份旧的 Fiber 对象
    1. 在初始渲染结束后(commitAllWork())将根节点的 Fiber 对象存储在真实 DOM 上(__rootFiberContainer
    2. 在构建根节点 Fiber 时(getFirstTask())将旧的根节点 Fiber 对象备份到 alternate 属性
    3. 在构建子节点时(reconcileChildren())备份旧的子节点 Fiber,还要根据操作构建不同操作类型的 Fiber 节点对象
      1. 首先判断父级是否有 alternate
      2. 如果有则获取 alternate 的子级(child),它是循环的第一个子节点的备份
      3. 循环子级节点,判断节点是否有对应的备份
        1. 如果有则为更新节点操作
          1. 将备份存储到alternate
          2. 判断节点类型是否相同
            1. 如果不同则需要重新获取 stateNode
            2. 如果相同则直接取 alternatestateNode
        2. 如果没有,则不需要对alternate赋值
      4. 接着判断alternate是否有兄弟节点
        1. 如果有则将兄弟节点作为下一轮循环的子节点的备份
  2. 构建完 Fiber 后操作 DOM 对象,commitAllWork()中循环根节点的 effects,也就是所有的 Fiber 对象,判断它的操作类型(effectTag):
    1. update 更新节点操作
      1. 判断节点类型
        1. 相同节点,执行更新节点操作
          1. 文本节点更新文本内容
          2. 其它节点更新它们的属性
        2. 不同节点,直接用新节点替换就节点
    2. placement 追加节点操作

实现节点删除

当前仅处理普通节点的删除

示例

// src/index.js
import React, { render, Component } from './react'

const root = document.getElementById('root')

const jsx = (
  <div>
    <p>Hello React</p>
    <p>Hi Fiber</p>
  </div>
)

render(jsx, root)

setTimeout(() => {
  const jsx = (
    <div>
      {/* <h1>你好 React</h1> */}
      <p>Hi Fiber</p>
    </div>
  )
  render(jsx, root)
}, 2000)

构建删除操作的 Fiber 节点对象

reconcileChildren() 中通过判断循环的判断当前如果是删除操作,就构建删除操作的 Fiber 节点对象。

  • 根据当前循环的子节点对应的 alternate是否存在, 判断节点是否被删除
  • 当子节点被清空的时候,子节点的数量为0,无法进入循环,所以要为 while 循环增加一个判断条件,判断是否有子级的备份
  • 并在进入循环后,判断当前子节点是否存在,以判断节点是否被删除
  • 当为删除节点操作时,将当前节点的备份 Fiber 中的 effectTag 设置为 delete 添加到 effects 中,在最终执行 DOM 操作的时候会处理
  • 在为上一个子节点设置兄弟节点的时候要判断当前节点是否存在,如果不存在则不设置兄弟节点
const reconcileChildren = (fiber, children) => {
  /*...*/

  while (index < numberOfElements || alternate) {
    // 子级 virtualDOM 对象
    element = arrifiedChildren[index]

    if (!element && alternate) {
      /* 删除操作 */
      alternate.effectTag = 'delete'
      fiber.effects.push(alternate)
    } else if (element && alternate) {
      /* 更新操作 */
      /*...*/
    } else if (element && !alternate) {
      /* 初始渲染操作 */
      /*...*/
    }

    // 指明父子关系、兄弟关系
    if (index === 0) {
      // 父节点的子节点只能是第一个子节点
      fiber.child = newFiber
    } else if (element) {
      // 其它的节点作为上一个节点的兄弟节点
      prevFiber.sibling = newFiber
    }

    /*...*/
  }
}

执行 DOM 删除操作

commitAllWork() 中判断,如果是删除操作,直接调用父节点的 removeChild()删除当前节点即可:

const commitAllWork = fiber => {
  // 循环 effects 数组 构建 DOM 节点树
  fiber.effects.forEach(item => {
    if (item.effectTag === 'delete') {
      /* 删除节点 */
      item.parent.stateNode.removeChild(item.stateNode)
    } else if (item.effectTag === 'update') {
      /* 更新节点 */
      /*...*/
    } else if (item.effectTag === 'placement') {
      /* 追加节点 */
      /*...*/
    }
  })

  /*...*/
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值