Skip to content

通过一份mini版的react核心驱动代码,来快速学习React原理,使我们快速地了解React的底层原理和驱动模式,方便我们迅速掌握React的基本设计思想和关键流程,内附有详细的笔记讲解。

Notifications You must be signed in to change notification settings

Suzumiya-Tiger/React-internals-fast-track

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

安装和启动项目

  1. 执行pnpm install 安装简单的必要依赖。

  2. 执行npx tsc

    npx tsc意味着:

  • 执行 TypeScript 编译器

  • 将 TypeScript (.ts/.tsx) 文件编译成 JavaScript

  • 根据项目中的 tsconfig.json 配置进行编译

  • 在没有全局安装 TypeScript 的情况下也能临时使用编译器

  1. 执行npx http-server .:

    npx http-server . 意味着:

  • 在当前目录启动一个简单的 HTTP 服务器

  • 使当前目录的文件可以通过浏览器访问

  • 解决直接打开 HTML 文件可能遇到的跨域问题

  • 便于在本地测试 mini-react 应用

接下来配合启动的项目,开启React底层原理的学习之旅:

整体架构与目的

这是一个简化版 React 实现(Mini-React),目标是复制 React 的核心功能和工作原理,包括:

  • Fiber 架构实现异步可中断渲染

  • 函数组件支持

  • Hooks API (useState, useEffect)

  • 虚拟 DOM 和 DOM 操作

  • 协调算法 (Reconciliation)

核心数据结构

全局状态变量

let nextUnitOfWork = null     // 下一个工作单元,指向待处理的Fiber节点
let wipRoot = null            // 当前正在构建的Fiber树根节点
let currentRoot = null        // 上一次渲染完成的Fiber树根节点
let deletions = null          // 需要删除的节点列表
let wipFiber = null           // 当前正在处理的函数组件对应的Fiber节点
let stateHookIndex = null     // 当前组件内Hook的索引

Fiber 节点结构

每个 Fiber 节点代表一个工作单元,包含以下属性:

{
  type: string | function,  // 元素类型:DOM标签名或函数组件
  props: {                  // 元素属性
    children: [],           // 子元素数组
    ...otherProps           // 其他属性和事件处理器
  },
  dom: HTMLElement | null,  // 对应的真实DOM节点
  return: Fiber,            // 父Fiber节点
  child: Fiber,             // 第一个子Fiber节点
  sibling: Fiber,           // 下一个兄弟Fiber节点
  alternate: Fiber,         // 上一次渲染的对应Fiber节点
  effectTag: string,        // 操作标记:"PLACEMENT"|"UPDATE"|"DELETION"
  stateHooks: [],           // 状态钩子数组(函数组件)
  effectHooks: []           // 副作用钩子数组(函数组件)
}

渲染流程详解

初始化渲染
↓
[render(element, container)]
↓
创建根 Fiber 节点 (wipRoot)
↓
设置 nextUnitOfWork = wipRoot
↓
[workLoop] ← requestIdleCallback
↓
处理当前 Fiber 节点 [performUnitOfWork]
  ↙            ↘
函数组件       普通元素
[updateFunctionComponent]  [updateHostComponent]
       ↓                 ↓
  处理子元素 [reconcileChildren]
       ↓
  返回下一个工作单元
       ↓
所有工作单元处理完毕
       ↓
[commitRoot] 提交阶段
  ↙     ↓     ↘
删除节点  更新节点  添加节点
  ↓
执行副作用 [commitEffectHooks]
  ↓
更新 currentRoot
  ↓
渲染完成

1. 初始化渲染

  /**
   * 渲染流程
   * 初始化阶段:render函数创建第一个fiber节点(wipRoot),并设置nextUnitOfWork
   * 任务调度:workLoop函数使用requestIdleCallback在浏览器空闲时执行渲染工作
   * 构建Fiber树:performUnitOfWork函数根据当前fiber节点创建新的fiber节点,并设置nextUnitOfWork
   * 提交阶段:commitRoot函数在任务完成后将Fiber树提交到DOM
   * 
   * 使用Fiber树表示组件树,每个Fiber节点是一个工作单元
   * 通过requestIdleCallback在浏览器空闲时执行渲染工作
   * 采用深度优先遍历处理Fiber树
   * 渲染分为两个阶段:
   * Reconciliation/Render阶段:可中断,构建Fiber树
   * Commit阶段:不可中断,将变更应用到DOM
   
   */
  
  // React.render(<App />, document.getElementById('root'));
  // 在这个调用中:
  // <App /> 是根元素节点
  // document.getElementById('root') 是容器节点
  // 然后在 render 函数内部,这个根元素被包装到一个特殊的 Fiber 节点结构中:
  function render(element, container) {
    wipRoot = {
      // 根节点
      dom: container,
      props: {
        children: [element]  // 初次渲染的时候,根元素被放在这里
      },
      alternate: currentRoot  // 指向上一次渲染的树
    }
  
    deletions = []
  
    nextUnitOfWork = wipRoot
  }
  1. 创建根 Fiber 节点(wipRoot)

  2. 初始化 deletions 数组

  3. 设置 nextUnitOfWork 开始渲染过程

currentRoot 是指向当前已渲染到屏幕上的 Fiber 树根节点的引用,它在整个 Mini-React 实现中扮演着关键角色。

currentRoot 的作用

  1. 保存当前状态:currentRoot 保存着当前显示在屏幕上的 Fiber 树,它代表了应用的当前状态。

  2. 对比更新的基础:当状态发生变化需要重新渲染时,新的 Fiber 树(wipRoot)会通过 alternate 属性引用 currentRoot,从而能够对比新旧两棵树的差异。

  3. 实现协调算法:React 的核心是高效地确定哪些部分需要更新。通过比较 wipRoot 和 currentRoot,Mini-React 可以只更新真正变化的部分,而不是重建整个 DOM。

为什么初始值为 null

currentRoot 初始值为 null 的原因:

  1. 首次渲染:当应用首次渲染时,屏幕上还没有任何内容,所以没有"当前显示的 Fiber 树"。
  2. 区分初始渲染和更新:初始为 null 让我们可以区分是首次渲染还是更新操作。在 reconcileChildren 函数中,如果 wipFiber.alternate 为 null(即首次渲染),就不需要进行比较,直接创建新节点。
  3. 循环完成:首次渲染完成后,commitRoot 函数会将 wipRoot 赋值给 currentRoot,完成渲染循环:
   currentRoot = wipRoot
   wipRoot = null

渲染循环图解

初始渲染:
1. currentRoot = null
2. render() 创建 wipRoot
3. 构建并提交 Fiber 树
4. commitRoot() 设置 currentRoot = wipRoot

更新渲染:
1. 状态更新触发重新渲染
2. 创建新的 wipRoot,设置 wipRoot.alternate = currentRoot
3. 构建新 Fiber 树时通过 alternate 比较差异
4. commitRoot() 应用变更并更新 currentRoot

这种设计形成了一个完整的渲染循环,使得 Mini-React 能够高效地处理状态更新并反映到 UI 上,同时保持对 DOM 操作的最小化。

2. 工作循环

  function workLoop(deadline) {
    let shouldYield = false
    // 循环处理工作单元,直到没有工作或需要让出控制权
    while (nextUnitOfWork && !shouldYield) {
      // 处理当前工作单元
      nextUnitOfWork = performUnitOfWork(
        nextUnitOfWork
      )
      // 检查剩余时间
      shouldYield = deadline.timeRemaining() < 1
    }
    // 如果所有工作单元都处理完毕且有根节点,则提交根节点
    if (!nextUnitOfWork && wipRoot) {
      commitRoot()
    }
  
    requestIdleCallback(workLoop)
  }
  requestIdleCallback(workLoop)
  1. 使用 requestIdleCallback 在浏览器空闲时执行任务

  2. 循环处理工作单元直到时间用完或工作全部完成

  3. 当所有工作完成后(nextUnitOfWork 为 null),调用 commitRoot 提交更改

3. 工作单元处理

  // 这是 Fiber 架构的核心函数,负责处理每个工作单元(Fiber 节点)并返回下一个要处理的工作单元。
  function performUnitOfWork(fiber) {
    // 判断是函数组件还是普通 DOM 元素
    const isFunctionComponent = fiber.type instanceof Function
  
    if (isFunctionComponent) {
      updateFunctionComponent(fiber)
    } else {
      updateHostComponent(fiber)
    }
    // 开始执行深度优先遍历:优先返回子节点
    if (fiber.child) {
      return fiber.child
    }
  
    // 没有子节点就找兄弟节点
    let nextFiber = fiber
    while (nextFiber) {
      if (nextFiber.sibling) {
        return nextFiber.sibling
      }
      // 没有兄弟节点就回到父节点
      nextFiber = nextFiber.return
    }
  }
  
  1. 区分函数组件和普通 DOM 元素

  2. 执行相应的更新操作

  3. 实现深度优先遍历:优先处理子节点,然后是兄弟节点,最后回到父节点

4. DOM 元素处理

  function updateHostComponent(fiber) {
    // 如果 DOM 还未创建,就创建它
    if (!fiber.dom) {
      fiber.dom = createDom(fiber)
    }
    // 协调子元素
    reconcileChildren(fiber, fiber.props.children)
  }
  1. 如果 DOM 不存在,创建 DOM 元素
  2. 协调子元素

5. 函数组件处理

  // 这两个变量用于管理函数组件的状态和副作用。
  let wipFiber = null      // 当前正在工作的函数组件对应的 Fiber
  /**
   * function Counter() {
    const [count, setCount] = useState(0);  // hook索引0
    const [name, setName] = useState("John"); // hook索引1
  }
   
   * 每次组件渲染时:
   * 首先 stateHookIndex 被重置为 0
   * 第一个 useState 调用使用索引 0,对应 count 状态
   * 调用后 stateHookIndex++ 变为 1
   * 第二个 useState 调用使用索引 1,对应 name 状态
   * 以此类推
   * 这就是为什么 React 要求 Hooks 必须在顶层使用,不能在条件语句中调用 
   * - 因为索引必须在每次渲染时保持一致,才能正确关联到对应的状态。
  
   */
  let stateHookIndex = null // 当前组件内 Hook 的索引
  
  /**
   * 关键点:
   * 函数组件本质上是一个函数,通过 fiber.type(fiber.props) 调用得到其返回的 React 元素
   * 初始化数组来存储状态 Hooks 和副作用 Hooks
   * 最后调用 reconcileChildren 处理子元素
   */
  function updateFunctionComponent(fiber) {
    // 设置当前工作中的函数组件 Fiber
    wipFiber = fiber
    // 重置 Hook 索引
    stateHookIndex = 0
    // 初始化状态 Hooks 和副作用 Hooks 数组
    wipFiber.stateHooks = []
    wipFiber.effectHooks = []
    
    let children = []
    // 防御性检查,确保 fiber.type 是函数
    if (typeof fiber.type === 'function') {
      // 调用函数组件得到子元素
      children = [fiber.type(fiber.props)]
    }
    
    // 协调子元素
    reconcileChildren(fiber, children)
  }
  
  1. 设置当前工作 Fiber 为 wipFiber

  2. 重置 Hooks 索引和数组

  3. 执行函数组件获取其子元素

  4. 协调子元素

函数组件的执行原理解析

fiber.type(fiber.props) 是函数组件的执行过程,而 children = [fiber.type(fiber.props)] 将结果包装成数组。这是函数组件处理的核心机制,让我详细解释:

fiber.type(fiber.props) 含义

当 fiber.type 是一个函数时,这就表示当前处理的是一个函数组件。在 React 中,函数组件本质上就是一个接收 props 并返回 React 元素的函数。

// 函数组件示例
function MyComponent(props) {
  return <div>Hello, {props.name}</div>;
}

在这个实现中:

  • fiber.type 就是函数组件本身(如 MyComponent 函数)

  • fiber.props 是传递给函数组件的属性

  • fiber.type(fiber.props) 就是执行这个函数,获取其返回的 React 元素

为什么需要包装成数组 [fiber.type(fiber.props)]

函数组件执行后返回的是单个 React 元素,但 reconcileChildren 函数需要处理子元素列表。将返回值包装成数组的原因是:

  1. 统一接口:reconcileChildren 函数期望接收一个元素数组作为参数

  2. 处理一致性:普通 DOM 元素的 children 本身就是数组,这样保持了处理逻辑的一致性

  3. 简化实现:即使函数组件只返回一个元素,将其放入数组可以用相同的循环逻辑处理

实际工作流程

当处理一个函数组件(例如 )时:

  1. fiber.type 是 Counter 函数本身

  2. fiber.props 是传递给 Counter 的属性

  3. 执行 Counter(props) 得到返回的 React 元素(如

    Count: 0

  4. 将这个返回值包装成数组 [

    Count: 0
    ]

  5. 将这个数组传递给 reconcileChildren,后者会为这个返回的元素创建新的 Fiber 节点

这个机制使得函数组件能够:

  • 根据传入的 props 动态生成元素结构

  • 使用 hooks 管理状态和副作用

  • 在每次状态变化时重新执行并生成新的元素树

这是 React 声明式编程模型的核心实现机制,让开发者可以通过编写描述"UI 应该是什么样子"的函数,而不必关心 DOM 操作的细节。

6. 协调子元素reconcileChildren

      /**
       * 当 performUnitOfWork 处理一个 Fiber 节点时,
       * 会调用 updateFunctionComponent 或 updateHostComponent
       * 
       * 这些函数会调用 reconcileChildren 来处理子元素
       * 
       * reconcileChildren 会比较新旧元素,创建新的 Fiber 节点,并设置适当的 effectTag
       * 
       * 当 workLoop 检测到所有工作单元处理完毕,会调用 commitRoot 来应用这些效果
       * 
       * commitRoot 会根据 effectTag 执行相应的 DOM 操作
       * 
       * wipFiber(work in progress Fiber):
       * 来源:从 updateFunctionComponent 或 updateHostComponent 传入
       * 作用:当前正在工作的 Fiber 节点,我们要为它创建或更新子节点
       * elements:
       * 来源:
       * 对于函数组件:是函数组件执行结果 [fiber.type(fiber.props)]
       * 对于普通元素:是 fiber.props.children
       * 作用:新的子元素列表,需要与旧的 Fiber 节点进行比较
       */
  function reconcileChildren(wipFiber, elements) {
    // index:当前处理的子元素索引
    let index = 0
    /* oldFiber:
       * 来源:通过 wipFiber.alternate?.child 获取,是上一次渲染的对应子节点
       * 作用:用于与新元素比较
       */
    let oldFiber = wipFiber.alternate?.child
  
    // prevSibling:上一个处理过的兄弟节点,用于构建兄弟节点之间的链接
    let prevSibling = null
  
    // 函数使用一个 while 循环来处理所有子元素和所有旧的 Fiber 节点:
    while (index < elements.length || oldFiber != null) {
      const element = elements[index]
      let newFiber = null
  
      const sameType = element?.type == oldFiber?.type
      /**
       * effectTag 的作用
       * effectTag 是一个标记,用于指示在 commit 阶段应该对这个 Fiber 节点执行什么操作:
       * "UPDATE":更新现有的 DOM 节点
       * "PLACEMENT":创建新的 DOM 节点并插入
       * "DELETION":删除 DOM 节点
       */
      // 如果新旧元素类型相同,则创建一个更新操作的 Fiber 节点
      if (sameType && oldFiber && element) {
        newFiber = {
          type: oldFiber.type,
          props: element.props,  // 使用新的 props
           dom: oldFiber.dom,     // 复用旧的 DOM
           return: wipFiber,      // 指向父节点
           alternate: oldFiber,   // 指向旧节点
           effectTag: "UPDATE"    // 标记为更新
        }
      }
      // 如果新元素存在且类型不同,则创建一个新元素的 Fiber 节点
      if (element && !sameType) {
        newFiber = {
          type: element.type,
          props: element.props,
          dom: null,            // 需要创建新的 DOM
          return: wipFiber,
          alternate: null,      // 没有对应的旧节点
          effectTag: "PLACEMENT" // 标记为放置
        }
      }
      // 如果旧元素存在且类型不同,则标记为删除
      if (oldFiber && !sameType) {
        oldFiber.effectTag = "DELETION"
        deletions.push(oldFiber)
      }
      // 如果旧元素存在,则移动到下一个兄弟节点
      if (oldFiber) {
        oldFiber = oldFiber.sibling
      }
      // 如果当前是第一个子元素,则设置为当前 Fiber 的 child
      if (index === 0) {
        wipFiber.child = newFiber
      } 
      // 如果当前不是第一个子元素,则设置为上一个兄弟节点的兄弟节点
      else if (element) {
        prevSibling.sibling = newFiber
      }
      // 更新上一个兄弟节点
      prevSibling = newFiber
      index++
    }
  
  }
  1. 对比新旧节点,决定更新操作
  2. 对相同类型节点执行更新(effectTag: "UPDATE")
  3. 对新增节点标记为放置(effectTag: "PLACEMENT")
  4. 对删除的节点添加到 deletions 数组(effectTag: "DELETION")
  5. 构建新的 Fiber 节点关系(父子、兄弟)

再讲fiber

这是当前正在处理的 Fiber 节点,代表一个 DOM 元素。它包含了这个元素的所有信息,例如:

{
  type: "div",             // DOM 标签名
  props: { ... },          // 元素属性
  dom: (DOM引用),          // 对应的真实 DOM 节点
  return: (父Fiber),       // 指向父 Fiber 节点
  alternate: (旧Fiber),    // 上一次渲染的对应节点
  child: null,             // 第一个子节点(尚未创建)
  sibling: null            // 下一个兄弟节点(尚未设置)
}

fiber.props.children

这是当前元素的所有子元素组成的数组,它们还只是虚拟 DOM 元素(React 元素),还没有转换为 Fiber 节点。

举例说明

假设有以下 JSX 结构:

<div className="container">
  <h1>Hello</h1>
  <p>World</p>
</div>

当处理 div 元素时:

fiber 参数

{
  type: "div",
  props: {
    className: "container",
    children: [
      { type: "h1", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "Hello", children: [] } }] } },
      { type: "p", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "World", children: [] } }] } }
    ]
  },
  dom: (div DOM 引用),
  return: ( Fiber),
  alternate: ( Fiber)
}

fiber.props.children 参数

[
  { type: "h1", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "Hello", children: [] } }] } },
  { type: "p", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "World", children: [] } }] } }
]

reconcileChildren 的目的

这个函数的作用是将子 React 元素转换为 Fiber 节点,并建立它们之间的关系:

  1. 对每个子元素创建或更新 Fiber 节点

  2. 设置父子关系 (return 指向父节点)

  3. 设置兄弟关系 (sibling 指向下一个兄弟节点)

  4. 标记更新类型 (effectTag: "PLACEMENT", "UPDATE" 或 "DELETION")

这样就完成了从虚拟 DOM 元素到 Fiber 节点的转换,构建了完整的 Fiber 树结构,为后续的 DOM 更新提供了基础。

  if (oldFiber) {
        oldFiber = oldFiber.sibling
      }

这里不需要往子节点探索,因为reconcileChildren函数的目的是处理当前Fiber节点(wipFiber)的直接子元素,而不是递归处理整个树。

这段代码中oldFiber = oldFiber.sibling是在水平方向遍历兄弟节点,而不是往深度方向探索子节点。原因是:

  1. reconcileChildren函数只负责一层子元素的协调工作,即当前Fiber节点的直接子元素

  2. 处理完一个子元素后,需要移动到下一个兄弟节点继续比较

  3. 深度遍历(子节点的子节点)是在后续的performUnitOfWork中完成的

整个Fiber树的构建是通过多次调用reconcileChildren实现的:

  • 每次处理一个Fiber节点时,reconcileChildren只处理其直接子元素

  • 当这些子元素被处理完并转化为新的Fiber节点后

  • performUnitOfWork会继续处理这些新创建的Fiber节点的子元素

这种设计允许React将渲染工作分解成小的工作单元,从而实现可中断的渲染过程。

updateDom

  /**
   * 这个函数负责更新 DOM 元素的属性和事件处理器,分四步进行:
   * 
   * 移除旧事件监听器:找出所有旧的事件属性,如果在新属性中不存在或值发生变化,则移除对应的事件监听器
   * 删除旧属性:找出所有在新属性中不存在的普通属性,将它们设置为空字符串
   * 设置新属性:找出所有值发生变化的普通属性,将它们设置到 DOM 元素上
   * 添加新事件监听器:找出所有值发生变化的事件属性,为它们添加事件监听器
   * 
   * 关键特点
   * 属性处理的细粒度控制:区分普通属性和事件处理器,分别处理
   * 高效更新:只更新发生变化的属性,避免不必要的 DOM 操作
   * 事件处理:正确处理事件监听器的添加和移除,避免内存泄漏
   * 函数式编程:使用函数式编程风格(过滤、映射等)处理属性
  这部分代码实现了虚拟 DOM 到真实 DOM 的映射和更新,是实现声明式 UI 的核心机制。
   */
  function updateDom(dom, prevProps, nextProps) {
    // 移除旧的或变化的事件监听器
    Object.keys(prevProps)
      .filter(isEvent)
      .filter(
        key => !(key in nextProps) || isNew(prevProps, nextProps)(key)
      )
      .forEach(name => {
        const eventType = name.toLowerCase().substring(2)
        dom.removeEventListener(eventType, prevProps[name])
      })
  
    // 删除旧的属性
    Object.keys(prevProps)
      .filter(isProperty)
      .filter(isGone(prevProps, nextProps))
      .forEach(name => {
        dom[name] = ""
      })
    // 设置或更新属性
  
    Object.keys(nextProps)
      .filter(isProperty)
      .filter(isNew(prevProps, nextProps))
      .forEach(name => {
        dom[name] = nextProps[name]
      })
  
    // 添加事件监听器
    Object.keys(nextProps)
      .filter(isEvent)
      .filter(isNew(prevProps, nextProps))
      .forEach(name => {
        const eventType = name.toLowerCase().substring(2)
        dom.addEventListener(eventType, nextProps[name])
      })
  
  }

作用:更新DOM元素的属性和事件处理器

参数:

  • dom: 要更新的DOM元素

  • prevProps: 旧属性对象

  • nextProps: 新属性对象

核心逻辑:

  1. 移除旧的或变化的事件监听器

  2. 删除不再存在的属性

  3. 设置新的或变化的属性

  4. 添加新的事件监听器

Object.keys(prevProps)
  .filter(isEvent)  // 只处理事件属性(on开头的)
  .filter(
    key => !(key in nextProps) || isNew(prevProps, nextProps)(key)
  )
  .forEach(name => {
    const eventType = name.toLowerCase().substring(2)
    dom.removeEventListener(eventType, prevProps[name])
  })

当事件处理器引用发生变化时(如从onClick={handler1}变为onClick={handler2}),浏览器的事件系统要求我们必须:

  1. 先移除旧的事件处理器

  2. 再添加新的事件处理器

这是因为DOM的addEventListener不会自动替换同类型的监听器,而是会累加。如果不先移除,就会导致多个处理器同时响应同一事件。

在代码的后续部分(没有展示),会有添加新事件处理器的逻辑:

Object.keys(nextProps)
  .filter(isEvent)
  .filter(isNew(prevProps, nextProps))
  .forEach(name => {
    const eventType = name.toLowerCase().substring(2)
    dom.addEventListener(eventType, nextProps[name])
  })

提交阶段

commitRoot

  /**
   * Commit 阶段是 React 渲染过程的第二阶段,
   * 负责将 Reconciliation 阶段生成的 Fiber 树应用到实际 DOM 上。
   * 这是不可中断的同步过程。
   * 
   * deletions:需要从 DOM 中移除的节点列表
   * wipRoot:当前工作中的 Fiber 树根节点
   * currentRoot:更新后保存的 Fiber 树,用于下次渲染比较
   */
  function commitRoot() {
    // 处理删除操作:先处理所有要删除的节点 deletions.forEach(commitWork)
    deletions.forEach(commitWork)
    // 递归提交子树:从根节点的第一个子节点开始 commitWork(wipRoot.child)
    commitWork(wipRoot.child)
  
    commitEffectHooks()
    // 保存当前树:将完成的树保存为 currentRoot,用于下次渲染比较
    // 当前渲染完成的树被保存为 currentRoot
    currentRoot = wipRoot
    // 清理状态:重置 wipRoot 和 deletions 数组
    wipRoot = null
    deletions = []
  }

作用:提交阶段入口,将所有变更应用到DOM

重要操作:

  1. 处理需要删除的节点

  2. 递归提交整个Fiber树

  3. 执行副作用钩子

  4. 保存当前完成的树,用于下次渲染比较

  5. 重置相关状态

commitWork

  function commitWork(fiber) {
    if (!fiber) {
      return
    }
    // domParentFiber:寻找到的带有 DOM 引用的父级 Fiber 节点
    let domParentFiber=fiber.return
  // 不断向上找,直到找到可以挂载DOM的父级节点
    while(!domParentFiber.dom){
      domParentFiber=domParentFiber.return
    }
    // domParent:实际的父级 DOM 元素,用于添加/移除子节点
    const domParent=domParentFiber.dom
  
    // 按照增增删改的 effectTag 来分别做处理
    /* domParent.appendChild(fiber.dom) 的作用是将 fiber.dom 作为子节点添加到 domParent 的子节点列表末尾。
    这是一个"添加"操作,而非"替换"操作。 */
    if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
      domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
      updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  } else if (fiber.effectTag === "DELETION") {
      commitDeletion(fiber, domParent)
  }
  // 递归执行
    commitWork(fiber.child)
    commitWork(fiber.sibling)
  }

作用:根据effectTag执行相应的DOM操作

参数:

  • fiber: 当前处理的Fiber节点

核心逻辑:

  1. 查找有DOM引用的父节点

  2. 根据effectTag执行添加、更新或删除操作

  3. 递归处理子节点和兄弟节点

commitDeletion

  function commitDeletion(fiber, domParent) {
    if (fiber.dom) {
        domParent.removeChild(fiber.dom)
    } else {
        commitDeletion(fiber.child, domParent)
    }
  }

作用:执行DOM删除操作,支持函数组件

参数:

  • fiber: 要删除的Fiber节点

  • domParent: 父DOM元素

核心逻辑:

  1. 如果当前节点有DOM引用,直接从父元素中移除

  2. 如果没有DOM引用(函数组件),递归处理子节点

Hooks实现

useState

 function useState(initialState){
    // 获取当前函数组件对应的 Fiber 节点: const currentFiber = wipFiber
    /**
     * wipFiber: 当前正在处理的函数组件对应的 Fiber 节点
     * 在 updateFunctionComponent 中设置
     * 用于跟踪 Hooks 所属的组件上下文
     */
    const currentFiber=wipFiber
    // 尝试从上一次渲染中获取对应位置的 Hook
    const oldHook=wipFiber.alternate?.stateHooks[stateHookIndex];
  
    // 创建新的 Hook 对象,其状态值基于旧 Hook 或初始值:
    const stateHook={
      state: oldHook ? oldHook.state : initialState,
      queue: oldHook ? oldHook.queue : [],
    }
  
    // 批量处理所有排队的状态更新操作:
    /**
     * 存储调用 setState 时传入的更新操作
     * 下一次渲染时批量执行这些操作
     */
   stateHook.queue.forEach((action) => {
    stateHook.state = action(stateHook.state);
  });
  
  // 清空更新队列
   stateHook.queue = [];
  
  //  增加 Hook 索引
  /**
   * stateHookIndex: 当前组件内 Hook 的索引
   * 每个组件内的 Hook 都有一个固定顺序
   * 不能在条件语句中使用 Hook 的原因 
   */
   stateHookIndex++;
  //  将 Hook 添加到当前组件的 Hooks 列表
   wipFiber.stateHooks.push(stateHook);
  
   function setState(action){
    /**
     * 支持两种更新方式:
     * 直接传值: setState(newValue)
     * 传递函数: setState(prevState => newState)
     * 更新操作加入队列,但不立即执行
     * 创建新的 wipRoot 并设置 nextUnitOfWork 触发重新渲染
     * 通过 alternate: currentFiber 保存当前状态,以便下次渲染时对比
     */
    const isFunction = typeof action === "function";
  
    stateHook.queue.push(isFunction ? action : () => action);
  
  /**
   * 调用 setState 后,我们需要触发一次新的渲染
   * 我们创建一个新的 wipRoot,基本上是当前 fiber 的复制
   * 设置这个新 fiber 的 alternate 指向当前 fiber
   * 这样在新的渲染过程中,新的 fiber 树可以通过 alternate 访问到当前状态
   * 本质上,这是在创建一个新的 "工作中" fiber 树,它的 alternate 指向当前的 fiber 树。
   * 这样在新的渲染循环中,我们能够对比新旧两棵树的差异。
   * 
     * 这里的关系是:
     * 新创建的 wipRoot:即将构建的新 fiber 树的根
     * currentFiber:当前存在的 fiber 节点
     * wipRoot.alternate = currentFiber:让新树能够引用到旧树
     * 这种设计确保了状态更新时能够启动一个新的渲染过程,并且能够正确比较新旧状态。
     */
    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    };
    nextUnitOfWork = wipRoot;
   }
  
   return [stateHook.state, setState];
  }

作用:实现React的useState钩子,提供状态管理能力

参数:

  • initialState: 初始状态值

返回值:[状态值, 状态更新函数]

核心逻辑:

  1. 从上一次渲染中恢复状态,或使用初始值

  2. 批量处理所有排队的状态更新

  3. 增加Hook索引并保存Hook

  4. 提供setState函数,将更新操作入队并触发重新渲染

    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    };
nextUnitOfWork = wipRoot

重新渲染的流程:

  1. 创建新的工作根节点:
  • wipRoot是"工作进行中的根节点"(work in progress root)

  • {...currentFiber}创建当前组件Fiber节点的浅拷贝

  • alternate: currentFiber建立新旧Fiber树之间的链接

  1. 设置下一个工作单元:
  • nextUnitOfWork = wipRoot告诉渲染系统从根节点开始处理
  1. 触发渲染循环:
  • 当nextUnitOfWork被设置后,正在运行的workLoop会检测到有新工作

  • workLoop会在浏览器空闲时开始处理这个新的工作单元

  • 整个组件树会被重新构建,发现变更,最终提交到DOM

关键点是重新设置了全局变量nextUnitOfWork和wipRoot,这两个变量控制着渲染系统的工作状态。Mini-React的渲染系统通过不断检查nextUnitOfWork是否存在来决定是否需要继续渲染工作。

在前面的代码我们可以看到,requestIdleCallback(workLoop)已经被调用,使workLoop函数持续运行。当setState设置了nextUnitOfWork后,workLoop检测到这个变量并开始处理,从而启动新一轮渲染。

useEffect

  /**
   * useEffect 函数
   * 
   * 参数:
   * callback: 回调函数,在组件挂载和更新时执行
   * deps: 依赖数组,当依赖发生变化时,回调函数会被重新执行
   * 
   */
  function useEffect(callback, deps) {
    const effectHook = {
      callback,
      deps,
      cleanup: undefined,
    };
    wipFiber.effectHooks.push(effectHook);
  }

作用:实现React的useEffect钩子,处理副作用

参数:

  • callback: 副作用回调函数

  • deps: 依赖数组

核心逻辑:

  1. 创建effect钩子对象

  2. 将钩子添加到当前函数组件的effectHooks数组

commitEffectHooks

  /**
   * useEffect 的工作原理
   * 两个阶段执行:
   * 先执行所有需要的清理函数
   * 再执行新的副作用函数
   * 依赖控制:
   * 无依赖数组:每次更新都执行
   * 空依赖数组([]):只在挂载和卸载时执行
   * 有依赖:依赖变化时执行
   * 
   * 清理机制:
   * 副作用函数可以返回一个清理函数
   * 在执行新的副作用前或组件卸载时调用清理函数
   * 执行时机:
   * 在 DOM 更新后异步执行
   * 确保副作用能够访问到最新的 DOM
   * 这种实现展示了 React 的 useEffect 的核心思想:通过依赖追踪管理副作用的执行,并在适当的时机执行清理工作。
   */
  function commitEffectHooks(){
      // 1. 定义执行清理函数的辅助函数
  
      /**
       * 流程和原理
       * 递归终止条件:如果 fiber 不存在,直接返回
       * 遍历旧 fiber 节点上的所有 effect hooks
       * 获取对应新 fiber 节点上的依赖数组
       * 如果依赖变化了,执行清理函数
       * 递归处理子节点和兄弟节点
       * 
       * 关键参数
       * fiber.alternate:上一次渲染的 fiber 节点
       * effectHooks:存储在 fiber 上的副作用钩子数组
       * hook.cleanup:上一次执行副作用函数时返回的清理函数
       * deps:依赖数组,决定何时重新执行副作用
       */
    function runCleanup(fiber){
      if(!fiber)return
      fiber.alternate?.effectHooks?.forEach((hook,index)=>{
        const deps=fiber.effectHooks[index].deps
  /**
   * 为什么在依赖变化时执行清理函数
     * 防止资源泄露:
     * 当依赖变化时,意味着副作用的上下文已经改变
     * 如果不清理,可能会导致内存泄露、定时器冲突、网络请求堆积等问题
     * 例如:如果 useEffect 中设置了定时器或订阅,不清理就会产生多个实例
     * 
     * 确保副作用的一致性:
     * 副作用应该与当前渲染的组件状态保持一致
     * 当依赖变化,需要先"撤销"旧的副作用,再建立新的副作用
     * 这样保证了在任何时刻,活跃的副作用都与当前状态匹配
     * 
     * 避免副作用冲突:
     * 不同依赖下的副作用可能有冲突行为
     * 清理确保新的副作用在"干净"的环境中执行
     */
        if(!hook.deps||!isDepsEqual(hook.deps,deps)){
          hook.cleanup?.()
        }
      })
  
      runCleanup(fiber.child);
      runCleanup(fiber.sibling);
    }
   // 2. 定义执行副作用的辅助函数
  
   /**
    * 流程和原理
     * 递归终止条件:如果 fiber 不存在,直接返回
     * 遍历当前 fiber 节点上的所有 effect hooks
     * 处理三种情况:
     * 首次渲染:直接执行副作用函数
     * 无依赖数组:每次更新都执行
     * 有依赖数组:比较依赖,变化时执行
     * 保存清理函数以便下次执行
     * 递归处理子节点和兄弟节点
     * 
     * 关键参数
     * newHook:当前渲染中的 effect hook
     * oldHook:上一次渲染中对应的 effect hook
     * hook.callback:用户传入的副作用函数
     * newHook.deps:当前的依赖数组
    */
    function run(fiber) {
      if(!fiber) return;
    
      fiber.effectHooks?.forEach((newHook, index) => {
        if(!fiber.alternate) {
          newHook.cleanup = newHook.callback();
          return;
        }
  
        if(!newHook.deps) {
          newHook.cleanup = newHook.callback();
        }
  
        if(newHook.deps && newHook.deps.length > 0) {
          const oldHook = fiber.alternate?.effectHooks?.[index];
          
          // 添加安全检查
          if(oldHook && oldHook.deps && !isDepsEqual(oldHook.deps, newHook.deps)) {
            newHook.cleanup = newHook.callback();
          }
        }
      });
  
      run(fiber.child);
      run(fiber.sibling);
    }
      // 3. 先执行清理函数
    runCleanup(wipRoot);
  
      // 4. 再执行新的副作用
    run(wipRoot);
  }

作用:执行副作用钩子的清理和回调函数

核心逻辑:

  1. 递归执行所有需要清理的副作用的清理函数
  2. 递归执行所有新的副作用函数
  3. 根据依赖变化决定是否执行

数据流与函数调用关系

初次渲染流程

  1. render(element, container):创建根Fiber节点,设置nextUnitOfWork
  2. workLoop(deadline):通过requestIdleCallback调度,检查是否有工作和时间
  3. performUnitOfWork(fiber):处理当前Fiber节点,返回下一个工作单元
  • 如果是函数组件,调用updateFunctionComponent(fiber)

  • 如果是普通元素,调用updateHostComponent(fiber)

  1. updateFunctionComponent(fiber)/updateHostComponent(fiber):
  • 处理当前节点(创建DOM或执行函数组件)

  • 调用reconcileChildren(fiber, children)处理子元素

  1. reconcileChildren(wipFiber, elements):比较协调子元素,创建子Fiber节点

  2. 当所有工作单元处理完毕,workLoop调用commitRoot()

  3. commitRoot():递归执行所有DOM操作,调用commitEffectHooks()

更新流程

  1. 用户调用setState(newState):
  • 更新操作被加入队列

  • 创建新的wipRoot,并设置nextUnitOfWork

  1. 同初次渲染的流程,但reconcileChildren会比较新旧节点

  2. 在commitRoot阶段,会根据effectTag执行相应的DOM更新

数据结构间的关系

Fiber节点字段关系

  • type & props:描述元素类型和属性

  • dom:与实际DOM的连接点

  • child, sibling, return:构成树状结构的链接

  • alternate:指向上一次渲染的对应节点,用于比较差异

  • effectTag:标记需要执行的DOM操作类型

全局变量关系

  • nextUnitOfWork:指向下一个要处理的Fiber节点

  • wipRoot:当前正在构建的新Fiber树

  • currentRoot:上一次渲染完成的Fiber树

  • deletions:需要删除的节点列表

  • wipFiber & stateHookIndex:处理Hooks时的上下文

详细流程图

初始化
┌───────────────────────────────────────┐
│ createElement(type, props, ...children)│◄────── JSX 转换
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ render(element, container)             │◄────── 应用入口
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 创建根Fiber节点:                       │
│ wipRoot = {                           │
│   dom: container,                     │
│   props: { children: [element] },     │
│   alternate: currentRoot              │
│ }                                     │
│ deletions = []                        │
│ nextUnitOfWork = wipRoot              │
└───────────────────┬───────────────────┘
                    ▼
任务调度         ┌───────────────────────────────────────┐
┌───────────────►│ workLoop(deadline)                    │◄─┐
│                └───────────────────┬───────────────────┘  │
│                                    ▼                      │
│                ┌───────────────────────────────────────┐  │
│                │ 检查: nextUnitOfWork 存在              │  │
│                │ 且 !shouldYield                        │  │
│                └───────────────────┬───────────────────┘  │
│                                    ▼                      │
│                ┌───────────────────────────────────────┐  │
│                │ performUnitOfWork(nextUnitOfWork)     │  │
│                └───────────────────┬───────────────────┘  │
│                                    ▼                      │
│                ┌───────────────────────────────────────┐  │
│                │ 检查: 是否所有工作单元处理完毕        │  │
│                │ 且wipRoot存在?                        │  │
│                └───────────────────┬───────────────────┘  │
│                                    ▼                      │
│                ┌───────────────────────────────────────┐  │
│                │ commitRoot() 执行提交                  │  │
│                └───────────────────┬───────────────────┘  │
│                                    ▼                      │
│                ┌───────────────────────────────────────┐  │
└────────────────┤ requestIdleCallback(workLoop)         │──┘
                 └───────────────────────────────────────┘

Fiber处理
┌───────────────────────────────────────┐
│ performUnitOfWork(fiber)              │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 判断: fiber.type instanceof Function   │
└───────────────────┬───────────────────┘
                    ▼
       ┌───────────────────────┐
       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐
│ 函数组件         │    │ 普通DOM元素     │
└────────┬────────┘    └────────┬────────┘
         ▼                      ▼
┌─────────────────┐    ┌─────────────────┐
│updateFunction   │    │updateHost       │
│Component(fiber) │    │Component(fiber) │
└────────┬────────┘    └────────┬────────┘
         │                      │
         │                      ▼
         │             ┌─────────────────┐
         │             │ 创建DOM (如需要) │
         │             └────────┬────────┘
         │                      │
         ▼                      ▼
┌─────────────────────────────────────────┐
│ reconcileChildren(fiber, children)      │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 深度优先遍历:                         │
│ 1. 返回child (如果存在)               │
│ 2. 否则返回sibling (如果存在)         │
│ 3. 否则回到parent找sibling            │
└───────────────────────────────────────┘

Hooks处理
┌───────────────────────────────────────┐
│ useState(initialState)                │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 1. 从alternate获取oldHook (如果存在)  │
│ 2. 创建新的stateHook对象              │
│ 3. 批量处理队列中的更新action         │
│ 4. 返回[state, setState]              │
└───────────────────────────────────────┘

┌───────────────────────────────────────┐
│ useEffect(callback, deps)             │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 1. 创建effectHook对象                 │
│ 2. 添加到wipFiber.effectHooks         │
└───────────────────────────────────────┘

提交阶段
┌───────────────────────────────────────┐
│ commitRoot()                          │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 1. 处理deletions中的节点              │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 2. 递归处理整个fiber树 (commitWork)   │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 3. 执行effect hooks (commitEffectHooks)│
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 4. 保存当前树: currentRoot = wipRoot   │
│    重置: wipRoot = null, deletions = []│
└───────────────────────────────────────┘

设计思想和目的

  1. 增量渲染:通过Fiber架构将渲染工作拆分为小单元,实现可中断的渲染过程,提高应用响应性
  2. 双缓冲技术:使用wipRoot和currentRoot两棵树,一个用于构建新的UI状态,一个表示当前屏幕上的状态,实现高效更新
  3. 协调算法:通过比较新旧元素类型,高效计算最小变更集,减少DOM操作
  4. 单向数据流:状态更新通过setState触发重新渲染,确保数据流向清晰可预测
  5. 组合优于继承:使用函数组件和Hooks实现组合式UI构建,避免继承带来的复杂性
  6. 声明式UI:开发者描述UI应该是什么样子,而不是直接操作DOM
  7. 时间切片:利用浏览器空闲时间执行渲染工作,避免长时间阻塞主线程

这个Mini-React实现虽然简化了很多细节,但成功捕捉了React的核心工作原理。这个实现对于理解React内部机制非常有价值,尤其是对于理解React的Fiber架构、协调算法、Hooks机制等核心概念。

常见疑问

useState中action是什么?

action是更新状态的指令,有两种形式:

  • 直接值:setState(5)

  • 更新函数:setState(prevState => prevState + 1)

代码会检查action类型:

const isFunction = typeof action === "function";
stateHook.queue.push(isFunction ? action : () => action);

如果是函数,直接使用;如果是值,将其包装成返回该值的函数。

2关于wipRoot的创建

wipRoot = {
  ...currentFiber,
  alternate: currentFiber,
};

这不是创建两个相同的fiber节点,而是实现React的"双缓冲"技术:

  • 创建一个新的"工作中"fiber树(wipRoot)

  • 让它引用当前fiber树(currentRoot)作为比较基准

  • 构建完成后,新树会替代旧树,成为下次更新的基准

这是为了确保状态更新能正确触发整个组件树的重新构建和比较。

updateFunctionComponent为什么没有创建DOM

函数组件本身不直接对应DOM节点。函数组件是一个返回React元素的函数。流程是:

  1. 执行函数组件得到React元素(虚拟DOM)
  2. 对这些元素调用reconcileChildren
  3. 如果元素是普通DOM元素,会在updateHostComponent中创建DOM

effect和useState的数据更新和识别时机

useState:

  • 在updateFunctionComponent中,执行函数组件时会调用useState

  • useState从上一次渲染的stateHooks中读取状态

  • 应用队列中的所有更新操作

  • 组件渲染出新内容,reconcileChildren识别变化

useEffect:

  • 在updateFunctionComponent中收集effect

  • 在commitRoot阶段的commitEffectHooks中执行

  • 先执行需要清理的旧effect的cleanup

  • 再执行需要更新的effect

wipRoot使用element作为children的原因

这是入口点设计:

  • 容器元素(如div#root)成为根Fiber节点

  • 应用的根组件(如)作为它的子元素

  • 这构建了一个统一的起点,让整个应用可以从这个根开始递归处理

fiber.type是什么?

fiber.type存储元素的类型:

  • 对于DOM元素:标签名字符串,如'div'、'span'

  • 对于函数组件:组件函数本身,如function App() {...}

  • 对于文本节点:特殊标识'TEXT_ELEMENT'

type用于:

  • 确定如何处理节点(updateFunctionComponent或updateHostComponent)

  • 创建正确类型的DOM元素

  • 在协调阶段比较元素类型是否相同

About

通过一份mini版的react核心驱动代码,来快速学习React原理,使我们快速地了解React的底层原理和驱动模式,方便我们迅速掌握React的基本设计思想和关键流程,内附有详细的笔记讲解。

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published