实现一个 Mini React:核心功能详解 - fiber 的迷你版实现 (2)

xdm,又要到饭了,又更新代码了!

总结一下上一篇完成的内容,

  1. 实现了mini fiber 的第一版,

有兴趣的可以点这里查看fiber 的迷你版实现 (1)

这篇将会更进一步实现fiber树。

1. 实现 Fiber 树结构

为了支持可中断的渲染,我们需要引入 Fiber 树 的概念。每个 Fiber 节点代表一个组件实例,包含组件的相关信息和状态。

Fiber 节点定义

js 复制代码
class FiberNode {
  constructor(type, props, parent = null, sibling = null, child = null) {
    this.type = type; // 组件类型
    this.props = props; // 组件属性
    this.parent = parent; // 父节点
    this.sibling = sibling; // 兄弟节点
    this.child = child; // 子节点
    this.effectTag = null; // 标记需要执行的操作(如更新)
    this.state = null; // 组件的状态(useState)
    this.hooks = []; // 组件的 Hook 列表
    this.currentHook = 0; // 当前 Hook 的索引
  }
}

Fiber 树的构建

在渲染过程中,我们会构建和遍历 Fiber 树。Fiber 树允许我们在渲染任务之间保存和恢复状态,从而实现可中断的渲染。

2. 拆分渲染任务

为了实现可中断的渲染,我们需要将渲染过程拆分为多个可中断的小任务。每次在浏览器的空闲时间(通过 requestIdleCallback 或其回退方案)执行一部分任务,然后暂停,等待下一次空闲时间继续执行剩余任务。

渲染的过程自顶向下,所以我们先创建一个fiber 的root 节点,根据上面的fiber 定义,我们可以先得出下面的代码

js 复制代码
let currentRoot = null

function renderElement(vdom, container) {
  // 创建根 Fiber
  
  const rootFiber = new FiberNode('ROOT', {children: vdom}, null, null, null)
  rootFiber.stateNode = container
  
  rootFiber.alternate = currentRoot
  
  if (currentRoot) {
      currentRoot.alternate = rootFiber
  }
  
  currentRoot = rootFiber;
  nextUnitOfWork = rootFiber;
  requestHostCallback(workLoop);
}
  1. nextUnitOfWork 设置下一个需要处理的fiber,首次进入就是根 fiber
  2. requestHostCallback 上一节已经完成了,其就是对 requestIdleCallback 的装饰。
  3. workLoop,一个函数, 接受一个参数。 参数可以获取/检查每一个frame剩余/空闲时间,决定是否继续执行更多的任务

2.1 实现 workLoop

js 复制代码
function workLoop(deadline) {
  while (nextUnitOfWork && deadline.timeRemaining() > 0) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  if (nextUnitOfWork) {
    requestHostCallback(workLoop);
  } else {
    commitRoot();
  }

  workLoopScheduled = false;
}

上面的workLoop函数循环的检查是否还有任务没有完成而且确认当前frame是否还有空闲时间。如果2个条件都满足,就会开始处理当前的fiber(组件),从函数代码

js 复制代码
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

可以知道,performUnitOfWork接受一个参数,这个参数就是待处理的当前fiber。performUnitOfWork 的返回值就是下一个需要处理的fiber。这里指的下一个fiber,就是当前fiber的子fiber,但是如果当前fiber 没有子fiber,那么返回当前fiber的兄弟fiber,如果也没有兄弟fiber,那么就返回null, 表示当前fiber处理完毕。

来实现一下performUnitOfWork。需要完成,

  1. 如果当前的fiber是根fiber,则可以使用fiber.props.children获取其子组件,props.children 通常包含的是组件的子组件。
  2. 如当前的fiber类型是一个函数函数,那么就表明当前fiber对应函数组件, 那么运行这个函数组件获取其虚拟dom(子组件),fiber(fiber.props), 运行返回值就是其虚拟dom(props.children 通常包含的是组件的子元素/组件)。
  3. 获取子组件 之后,就开始进行调和(reconcile),reconcile具体实现在下面
  4. 调和后,会返回这一层级的第一个子组件fiber进行performUnitOfWork(下一个需要处理工作单元, next unit of work)
  5. 如果没有子组件fiber,则尝试返回兄弟组件fiber,如果没有兄弟组件fiber,则进行回溯返回父fiber,以此类推直至根fiber,则返回null,遍历结束。
js 复制代码
function performUnitOfWork(fiber) {
  workInProgress = fiber
  if (fiber.type === 'ROOT') {
    const children =  fiber.props.children
    reconcileChildren(fiber, children)
  } else if (typeof fiber === 'function') {
    const component = fiber.type
    const props = fiber.props
    workInProgress.hooks = workInProgress.hooks || []
    workInProgress.currentHook = 0
      
    const children = component(props)
    reconcileChildren(fiber, children)
      
  } else {
    reconcileChildren(fiber, fiber.props.children)
  }
  
  if (fiber.child) {
    return fiber.child
  }
  
  let nextFiber = fiber
  
  while(nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent;
  }
  
  return null
}

进行到这里我们开始实现调和算法,

js 复制代码
function reconcileChildren(fiber, children)
{
    const oldFiberMap = {}
    let oldFiber = fiber.alternate ? fiber.alternate.child : null
    
    while(oldFiber) {
      const key = oldFiber.key || oldFiber.index
      oldFiberMap[key] = oldFiber
      oldFiber = oldFiber.sibling
    }
    
    let prevSibling = null
    let newFiber=  null
    let index = 0
    
    for(; index < children.length; index++) {
        const child = children[index]
        const key = child.key || index
        const matchedOldFiber = oldFiberMap[key]
        
        if (matchedOldFiber) {
            newFiber = createWorkInProgress(matchedOldFiber, child.props)
            newFiber.key = key
            newFiber.effectTag = 'UPDATE'
            delete oldFiberMap[key]
        } else {
          newFiber = new FiberNode(child.type, child.props, fiber)
          newFiber.key = key
          newFiber.effectTag = 'PLACEMENAT'
        }
        
        if (index === 0) {
            fiber.child = newFiber
        } else {
            prevSibling.sibling = newFiber
        }
        
        prevSibling = newFiber
    }
    
    Objects.values(oldFiberMap).forEach(fiberToDelete => {
      fiberToDelete.effectTag = 'DELETION'
    })
}

调和函数目的在于新的 vDOM 和旧的 Fiber 树之间建立联系,通过创建、更新和复用 Fiber 节点来优化渲染过程。这样可以确保 UI 的高效更新,减少不必要的 DOM 操作,从而提高性能和用户体验。reconcile的调用,

  1. 决定了performUnitOfWork 下一个需要处理的fiber,
  2. 这里使用了链表(prevSibling),使用sibling 记录当前处理的fiber的sibling。方便第三步进行遍历
  3. 处理fiber的顺序遵从相同等级的所有组件先创建对应的fiber,进行newProps的更新。接着返回这一层级第一个组件成为 next unit of work,接着第二个,以此类推。

这里选择使用链表的好处

  1. 高效深度优先遍历

使用链表结构使得 Fiber 树可以方便地进行深度优先遍历。这种遍历方式对于渲染和更新组件树非常重要,因为它允许逐层处理组件,从父组件到子组件,再到更深层的嵌套组件。

  • 子节点和兄弟节点的指针 :每个 Fiber 节点通过 child 指针指向其第一个子节点,通过 sibling 指针链接到下一个兄弟节点。这种结构使得遍历时无需使用额外的数据结构(如栈或队列),从而提高了遍历的效率。
  1. 支持可中断和可恢复的渲染

Fiber 的设计目标之一是实现可中断的渲染,这意味着渲染过程可以在任何时间点暂停,并在稍后的时间点恢复,以保证用户界面的响应性。

  • 链表结构的优势 :通过 childsibling 指针,Fiber 可以在任何节点处暂停渲染,然后从该节点继续。这种灵活性在处理长时间运行的渲染任务时尤为重要,因为它允许高优先级的任务(如用户交互)打断当前的渲染任务。
  • 工作单元的连续性:链表结构确保了每个工作单元(即每个 Fiber 节点)都可以独立处理和调度,不需要依赖于整个组件树的状态。这使得 Fiber 更容易管理和恢复中断的任务。

接着来完善 createWorkInProgress 函数,这个函数就会根据传入的fiber尝试在当前的fiber树获取对应的fiber(判断key) ,如果相同,则复用。如果没有,则创建。

js 复制代码
function createWorkInProgress(currentFiber, newProps) {
    let workInProgressFiber = currentFiber.alternate
    
    if (!workInProgressFiber) {
        workInProgressFiber = new FiberNode(currentFiber.type, newProps, currentFiber.parent)
        workInProgressFiber.alternate = currentFiber
        currentFiber.alternate = workInProgressFiber
    } else {
        workInProgressFiber.props = newProps
        workInProgressFiber.effectTag = null
    }
    
    
    
    workInProgressFiber.parent = currentFiber.parent
    workInProgressFiber.child = null; workInProgressFiber.sibling = null;      
    workInProgressFiber.stateNode = currentFiber.stateNode;
    workInProgressFiber.hooks = [...currentFiber.hooks]; 
    workInProgressFiber.currentHook = currentFiber.currentHook;
    
    return workInProgressFiber;
}

createWorkInProgress 根据传入的fiber尝试复用对应的fiber,如果不存在,才创建。接着根据新的props进行更新,最后复制其他数据比如 hooks, child, sibling, etc...

这一章节讲解了从0构建fiber树的创建的过程,包括其中使用的数据结构以及使用这样的数据结构有什么好处。

下一篇将继续完成fiber树更新真正dom的相关实现

如果这样的长度/强度你觉得可以接受,觉得有帮助,可以继续阅读下一篇,实现一个 Mini React:核心功能详解 - fiber 的迷你版实现(3)。

如果文章对你有帮助,请点个赞支持一下!

啥也不是,散会。

相关推荐
码农幻想梦1 小时前
实验九 视图的使用
前端·数据库·oracle
开心工作室_kaic3 小时前
ssm010基于ssm的新能源汽车在线租赁管理系统(论文+源码)_kaic
java·前端·spring boot·后端·汽车
大力水手~4 小时前
css之loading旋转加载
前端·javascript·css
Nguhyb4 小时前
-XSS-
前端·xss
前端郭德纲4 小时前
深入浅出ES6 Promise
前端·javascript·es6
就爱敲代码4 小时前
ES6 运算符的扩展
前端·ecmascript·es6
王哲晓5 小时前
第六章 Vue计算属性之computed
前端·javascript·vue.js
究极无敌暴龙战神X5 小时前
CSS复习2
前端·javascript·css
风清扬_jd5 小时前
Chromium HTML5 新的 Input 类型week对应c++
前端·c++·html5
Ellie陈5 小时前
Java已死,大模型才是未来?
java·开发语言·前端·后端·python