react 原理篇

159 阅读16分钟

1. 虚拟dom

Virtual Dom 是指用javascript去描述一个DOM结构,虚拟DOM不是直接去操作浏览器DOM,而是在虚拟DOM中对UI进行更新,减少不必要的真实DOM操作。

优点

  1. 性能优化:减少不必要的真实DOM操作,节省性能开销。(主要体现在diff算法等)
  2. 跨平台性:虚拟DOM是不受限平台的,不同平台可以做不同的映射目标。比如:虚拟DOM可以映射出微信小程序,electron,react , react native 多套UI。

Virtaul dom 简单实现

const React = {
  createElement (type, props, ...children) {
    return {
      type,
      props: {
        ...props,
        children: children.map(child => {
          if (typeof child === 'object') {
            return child
          } else {
            return React.createTextElement(child)
          }
        })
      }
    }
  },
  createTextElement (text) {
    return {
      type: 'TEXT_ELEMENT',
      props: {
        nodeValue: text,
        children: []
      }
    }
  }
}

const vdom = React.createElement('div', { title: 'KKK' }, React.createElement('H1', {}, 'nihao'))

console.log(JSON.stringify(vdom, null, 2))
/*
{
  "type": "div",
  "props": {
    "title": "KKK",
    "children": [
      {
        "type": "H1",
        "props": {
          "children": [
            {
              "type": "TEXT_ELEMENT",
              "props": {
                "nodeValue": "nihao",
                "children": []
              }
            }
          ]
        }
      }
    ]
  }
}
*/

2. react 的 Fiber 架构

React Fiber 是 React 16 中引入的新的核心协调(reconciliation)引擎

“Fiber”是对一个组件实例或 DOM 节点的抽象表示。

  1. 每个 React 组件都会对应一个 Fiber,包含相关的状态、属性、子节点等信息。
  2. Fiber 构成了一个树状结构(Fiber Tree),用于描述整个 UI 的层级关系。

Fiber 的能力/目标:

  1. ✅ 支持异步渲染(Concurrent Mode)
  2. ✅ 实现任务可中断/恢复
  3. ✅ 支持不同更新的优先级处理
  4. ✅ 支持双缓存树(Fiber Tree):Fiber 架构中有两棵Fiber树 current fiber tree(当前渲染的Fiber树) 和 work in progress fiber tree(正在处理的Fiber树)。React 使用这两棵树来保存更新前后的状态,从而更高效地进行比较和更新
  5. ✅ 支持任务切片:在浏览器的空闲时间内(利用 requestldleCallback思想),React 可以将渲染任务拆分成多个小片段,逐步完成 Fiber 树的构建,避免一次性完成所有渲染任务导致的阻塞。

Fiber 的工作原理

1. 虚拟 DOM 到 Fiber 树的映射:

  • Fiber 引入了一个可中断的渲染机制,渲染工作被分割成多个可以独立完成的小工作单元,由调度器来决定执行优先级和顺序。
  • 在旧版 React 中,使用的是“递归渲染”,即从根组件开始逐层向下渲染,无法中断。

2. Reconciliation(协调)阶段:Diffing 算法的升级,Fiber 使用了改进的 Diffing 算法,将更新操作分为两个阶段:

  • 👉 阶段一:Render Phase(构建 Fiber 树)
    • React 创建一个新的 Fiber 树(称为 workInProgress 树),基于当前的 JSX 和 props。
    • 这个阶段可以被中断,以便优先处理更高优先级的任务(如用户输入)。
  • 👉 阶段二:Commit Phase(提交到真实 DOM)
    • 当 Render 阶段完成后,React 将差异一次性更新到真实 DOM。
    • 此阶段不可中断,必须完整执行。

3. 任务调度与优先级管理

  • Fiber 引入了任务优先级的概念
  • React 使用内部的 Scheduler(调度器) 来决定哪些任务应该先执行,哪些可以延迟。

3. react工作流程(虚拟dom + fiber初始化,简单实现)

// virtaul dom
const React = {
  // type 标签类型,props 标签包含属性 children 是子元素
  createElement (type, props, ...children) {
    return {
      type,
      props: {
        ...props,
        children: children.map(child => {
          if (typeof child === 'object') {
            return child
          } else {
            return React.createTextElement(child)
          }
        })
      }
    }
  },
  createTextElement (text) {
    return {
      type: 'TEXT_ELEMENT',
      props: {
        nodeValue: text,
        children: []
      }
    }
  }
}

// 工作单元fiber初始化
let nextUnitWork = null // 下一个工作单元
let wipRoot = null  // 工作中的根节点,fiber树
let currentRoot = null // 当前fiber树(旧的fiber树,因为马上要替换)
let deletions = null // 要删除的fiber节点单元

function render (element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot //关联旧的fiber树
  }
  deletions = []
  nextUnitWork = wipRoot
}

// 工作单元fiber执行
function workloop (deadline) {
  let shouldYield = false // 是否有空闲时间
  // 有空闲时间  且  有任务
  while (nextUnitWork && !shouldYield) {
    nextUnitWork = performUnitWork(nextUnitWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  if (!nextUnitWork && wipRoot) {
    commitRoot()
  }
  requestIdleCallback(workloop)
}
requestIdleCallback(workloop)


// 创建fiber节点
const createFiber = (element, parent) => {
  return {
    type: element.type,
    props: element.props,
    parent: parent,
    dom: null,
    alternate: null,
    effectTag: null,
    sibling: null,
    child: null
  }
}

// 构建子节点fiber树,遍历子节点和兄弟节点,diff算法实现
const reconcileChildren = (fiber, elements) => {
  // 生成fiber three
  let index = 0
  let prevSibling = null // 上一个兄弟节点
  // diff 算法
  let oldFiber = fiber.alternate && fiber.alternate.child
  while (index < elements.length || oldFiber !== null) {
    const element = elements[index]
    let newFiber = null
    // 复用节点
    const someFiber = oldFiber && element && element.type === oldFiber.type
    if (someFiber) {
      console.log('复用节点', element)
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        parent: fiber,
        dom: oldFiber.dom,
        alternate: oldFiber,
        effectTag: 'UPDATE' // 更新
      }
    }
    // 新增节点
    if (element && !someFiber) {
      console.log('新增节点', element)
      newFiber = createFiber(element, fiber)
      newFiber.effectTag = 'PLACEMENT' // 新增
    }
    // 删除节点
    if (oldFiber && !someFiber) {
      console.log('删除节点', oldFiber)
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }
    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
    if (index === 0) {
      fiber.child = newFiber
    } else if (element) {
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
    index++
  }
  return null
}


function creatDom (fiber) {
  const dom = fiber.type === 'TEXT_ELEMENT' ?
    document.createTextNode('') :
    document.createElement(fiber.type)
  updateDom(dom, {}, fiber.props)
  return dom
}
const updateDom = (dom, prevProps, nextProps) => {
  // 删除旧属性
  Object.keys(prevProps).filter(key => key !== 'children').forEach(key => {
    dom[key] = ''
  })
  // 添加新属性
  Object.keys(nextProps).filter(key => key !== 'children').forEach(key => {
    dom[key] = nextProps[key]
  })
}

// 执行工作单元
const performUnitWork = (fiber) => {
  if (!fiber.dom) {
    fiber.dom = creatDom(fiber)
  }
  const elements = fiber.props.children
  reconcileChildren(fiber, elements)
  // 遍历子节点
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    // 如果有兄弟节点就返回兄弟节点
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    // 如果没有兄弟节点就返回上级节点
    nextFiber = nextFiber.parent
  }
  return null
}


const commitRoot = () => {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}

const commitWork = (fiber) => {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  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") {
    domParent.removeChild(fiber.dom)
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}


const vdom = React.createElement("div", { id: "1" }, React.createElement("span", null, "我靠"))
const vdom2 = React.createElement("div", { id: "2" }, React.createElement("H1", null, "你妹"))
render(vdom, document.getElementById('root'))
setTimeout(() => {
  render(vdom2, document.getElementById('root'))
}, 2000)

4. Diff 算法中,层级比较逻辑

  1. 按层级逐层比较:从根节点开始,逐层向下进行
  2. 同层节点按顺序比:对同一层级的节点,从左到右,按索引顺序一一对应比较
  3. 元素类型比较
    • 元素类型不相同:直接销毁旧节点及整个子树,新建新节点。
    • 元素类型相同:对比属性,props(包括 styleclassName、事件等)。
      • 属性不同:不重建节点,只更新变化的局部属性,其余保持不变。
      • 属性相同:保持不变
  4. 递归子节点:对子节点列表重复步骤 2-3

5. react 中的核心包

5.1. react

5.2. react-dom

5.3. react-reconciler

5.4. scheduler

6. react 工作原理(简述)

6.1. 初始化阶段

  1. 创建根节点
    1. 调用 ReactDom.createRoot(contianer) 创建 FiberRootNode 顶层根节点(根管理器)。
    2. 内部调用 createFiberRoot 初始化 FiberRootNode 结构,设置 current 指针指向初始的 Fiber 树根节点,并初始化其他必要字段,如:penddingLanes updateQueue
      • penddingLanes:集中追踪所有待处理的更新任务及其优先级
      • updateQueue:更新队列
  2. 挂载应用
    • 调用 render(element)<App/> 挂载到指定容器内。
    • 内部通过 scheduleUpdateOnFiber 函数,将一个更新任务加入到 FiberRootNode 的更新队列中。

6.2. 协调/渲染阶段 (Reconciliation / Render Phase - 构建 Fiber 树)

  1. 任务调度
    • Scheduler 调度器根据任务的优先级(通过 lanes 系统)来决定何时执行该任务。
    • 当有更高优先级的任务插入时,低优先级任务会被暂停或延迟执行。
  2. 进入工作循环
    • 当任务被调度时,进入递归的“工作循环” (work loop),开始处理 FiberRootNode 中的更新(并发模式使用 performConcurrentWorkOnRoot 处理更新)。
    • 函数会进入工作循环,开始处理 Fiber 树上的每个节点,检查它们是否需要更新,并进行必要的计算。
  3. 处理 Fiber 节点
    1. beginWork 函数,从 FiberRootNodecurrent 指针开始深度优先遍历 Fiber 树。
      • Fiber 每个节点调用 beginWork 函数,检查该节点是否需要更新。
      • beginWork 函数会根据节点类型(如:HostComponent、FunctionComponent、ClassComponent等)调用处理相应的处理函数。
    2. reconcileChildren 函数
      • 在 beginWork 中,对于需要更新的节点,调用 reconcileChildren 函数来比较新旧子节点。
      • reconcileChildren 使用 diffing 算法来找出需要添加、删除或更新的子节点,并生成相应的 Update 对象。
    3. completeWork 函数:
      • 当一个 Fiber 节点的所有子节点都处理完毕后,调用 completeWork 函数完成该节点的工作。
      • completeWork 会创建或更新对应的 DOM 节点,但不立即插入到页面。
  4. 这个阶段是可中断的(在并发模式下),如果浏览器需要处理更高优先级的任务(如用户输入),React 可以暂停当前工作,稍后恢复。

6.3. 提交阶段 (Commit Phase - 更新真实 DOM)

  • 提交更改
  1. 当整个 Fiber 树的“渲染/协调”阶段完成(生成了一个待提交的完成树 (finished work tree) ),进入不可中断的提交阶段
  2. 完成树替换为当前的当前树 (current tree)
  3. 调用 commitRoot 函数
    1. commitRoot 函数会遍历已完成的 Fiber 树
    2. 将所有变更应用到实际 DOM 上(commitMutationEffects 执行插入、删除、更新 DOM 节点等 突变操作)。
      1. appendChild: 将新创建的 DOM 节点一次性插入到根容器(如 #root)中。
      2. insertBeforeremoveChild 等。
    3. 并触发相应的生命周期方法或 Hooks 效果函数。
      1. commitLayoutEffects: 在DOM更新后、浏览器绘制前,执行useLayoutEffect的回调。
      2. useEffect: 将 useEffect 的回调放入微任务队列,等待浏览器绘制完成后执行。
  4. 浏览器绘制
    • 提交阶段完成后,浏览器引擎接管,将更新后的 DOM 树绘制到屏幕上,用户首次看到应用界面。

6.4. 更新队列 & 批量更新

  1. 更新队列管理
    • enqueueUpdate 函数: 当组件调用 setState 或 dispatch 时,enqueueUpdate 函数 会检查当前 Fiber 是否有正在进行的更新任务? 有,将其添加到相应 Fiber 节点的更新队列中;否则,会调用createUpdate创建一个新的Update对象,调度一个新的更新任务。
  2. 批量更新
    • batchedUpdates 函数: 在事件处理函数中,会自动启用批量更新机制。batchedUpdates会将多个setState调用合并成一个批次进行处理,减少不必要的重渲染和 DOM 操作。

6.5. 优先级调度

  • lanes 系统:
    • React 使用 lanes 系统来管理不同优先级的更新。
    • 每个更新都会被分配一个或多个 lane,表示其优先级。
    • scheduler 根据 lanes 来决定何时执行更新任务。

6.6. 并发模式

使用 createRoot 替代 ReactDOM.render 启用并发模式。

并发模式核心特性:

  1. 可中断的渲染:允许 React 在渲染过程中暂停较不重要的任务,以便优先处理更重要的任务。
  2. 优先级调度:基于 lanes 系统实现,React 根据任务的紧急程度来安排其执行顺序。
  3. 自动批处理:将多个状态更新合并成一个批次进行处理,减少不必要的重渲染次数。
  4. scheduler 会在浏览器空闲时间执行低优先级任务,以保证高优先级任务得到及时处理。

6.7. react 工作原理流程图

  • 基本流程图.png

7. react 中hooks为什么不能写在判断里?

React 需要确保每次组件渲染时都以相同的顺序调用 Hooks,如果根据条件逻辑跳过某些 Hook 调用,Hooks 的调用无法正确入栈,无法按顺执行。

  1. React 使用一种链表结构来跟踪和管理组件内的每个Hook状态,顺序的错乱会导致链表节点无法对齐
  2. Hooks 依赖于先前的渲染结果来维护组件的状态或执行相应的副作用,调用顺序不固定,会导致某些副作用未被正确执行或清理(内存泄漏)。

8. MessageChannel 消息通信

MessageChannel 是一种高效的跨上下文通信机制,利用两个端口进行消息传递。 它本质上是一个宏任务,但由于执行延迟极低,常被用来模拟微任务行为。常用于:

  1. Worker 与主线程之间的通信
  2. iframe 之间的通信
  3. 实现高性能、异步任务调度(如自定义微任务队列)
  4. 替代 requestIdleCallback() 或实现类似行为。

MessageChannel 代码通讯示例:

const channel = new MessageChannel()
channel.port1.onmessage = (res) => {
  console.log('channel.port1.onmessage : ', res.data)
}
setTimeout(() => {
  channel.port2.postMessage('我发送了一个消息')
}, 2000);

9. react 调度器(基于MessageChannel模拟react调度器)

模拟react调度器:

// 模拟react调度器,使用 MessageChannel 模拟 requestIdleCallback 来完成

const ImmediatePriority = 1         // 立即执行的优先级,优先级最高,如:点击事件
const UserBlockingPriority = 2      // 用户阻塞级别的优先级,如:滚动,校验
const NormalPriority = 3            // 一般优先级,如:render动画,异步请求
const LowPriority = 4               // 低优先级,如:数据埋点上报
const Idelpriority = 5              // 极低优先级,如:console

function getCurrentTime () {
  return performance.now()
}

class SimpleScheduler {
  constructor() {
    this.taskQueue = []             // 任务队列
    this.isPerformingTask = false   // 是否在执行任务
    const channel = new MessageChannel()
    this.port = channel.port2
    channel.port1.onmessage = this.performTaskUnitDeadline.bind(this)
  }
  schedulerCallback (priority, callback) {
    var time = getCurrentTime()
    this.port.postMessage('触发任务')           // 触发任务
    let timeout = null
    switch (priority) {
      case ImmediatePriority:
        timeout = -1
        break
      case UserBlockingPriority:
        timeout = 250
        break
      case NormalPriority:
        timeout = 5000
        break
      case LowPriority:
        timeout = 10000
        break
      case Idelpriority:
        timeout = 13132131
        break
      default:
        timeout = 5000
        break
    }
    const newTask = {
      callback,
      priority,
      expirationTime: time + timeout
    }
    this.push(this.taskQueue, newTask)
    if (!this.isPerformingTask) {
      this.isPerformingTask = true
      this.port.postMessage(null)
    }
  }
  performTaskUnitDeadline () {
    this.isPerformingTask = true
    this.teskloop()
    this.isPerformingTask = false
  }
  teskloop () {
    while (this.taskQueue.length > 0) {
      const currentTask = this.peak(this.taskQueue)
      if (currentTask) {
        const cb = currentTask.callback
        cb && cb()
        this.pop(this.taskQueue)
      } else {
        break
      }
    }
  }
  push (queue, task) {
    queue.push(task)
    // 排序
    queue.sort((a, b) => a.expirationTime - b.expirationTime)
  }
  peak (queue) {
    return queue[0] || null
  }
  pop (queue) {
    queue.shift()
  }
}


const s = new SimpleScheduler()

s.schedulerCallback(UserBlockingPriority, () => {
  console.log('2')
})

s.schedulerCallback(ImmediatePriority, () => {
  console.log('1')
})

10. react 为什么自己开发一个Scheduler调度器? 而不用 requestIdleCallback?

  1. 兼容性与控制力requestIdleCallback 浏览器支持有限,且调用频率低(约每秒30次),无法满足React高频、细粒度调度需求。
  2. 更灵活的优先级机制:React需实现不同优先级任务(如高优交互、低优渲染),Scheduler可自定义优先级策略,而requestIdleCallback仅提供基础空闲回调。
  3. 时间切片(Time Slicing) :Scheduler能在一帧内预留时间执行任务,实现任务中断与恢复,requestIdleCallback无法精确控制帧时间。
  4. 统一跨平台调度:Scheduler可在浏览器、服务端、原生App等环境统一运行,不依赖浏览器API。
  5. 更优的帧协调:结合requestAnimationFrame,Scheduler能更精准判断帧结束,避免requestIdleCallback延迟不可控的问题。

11. react 合成事件是什么?

React的合成事件(SyntheticEvent)系统是为了解决跨浏览器兼容性问题,并提供一个统一的接口来处理各种类型的事件。

原理

  1. 事件委托:组件内的元素绑定事件处理器时,通过事件委托的方式,将所有事件处理器都绑定到了根节点(通常是document),由React的事件系统进行处理。
  2. 合成事件对象:React创建一个跨浏览器的、与W3C标准兼容的事件对象,封装原生浏览器事件。提供与原生事件相似的接口,但消除了不同浏览器之间的差异,使得事件处理逻辑可以一致地运行在不同的浏览器环境中。
  3. 事件池机制:React使用了一个事件池来管理合成事件对象。事件触发后,React会从事件池中取出一个合成事件对象,填充它并将其传递给相应的事件处理器。事件处理器执行完毕,该合成事件对象会被回收至事件池中以便重用。这种机制减少了垃圾回收的压力,提高了性能。
  4. 跨平台抽象:React的合成事件不局限于Web平台,它也为其他平台(如React Native)提供了一致的事件处理模型,代码可以在不同平台上共享或更容易移植。

主要特点

  • 一致性:无论目标浏览器如何实现事件,React的合成事件都提供了一致的API和行为。
  • 性能优化:通过事件委托和事件池机制减少了内存消耗和事件处理器的数量,提高了性能。
  • 扩展性:允许框架自身以及第三方开发者对事件系统进行扩展,支持自定义事件类型等高级功能。

12. JSX 本质是什么? 为什么浏览器无法直接解析?

JSX(JavaScript XML)本质是为迎合声明式UI编程而设计的 JavaScript 语法扩展。(JSXJS的扩展语法)

为什么浏览器无法直接解析?

  • 浏览器只能执行符合ECMAScript标准的 JavaScript 代码。
  • JSX 代码在运行之前转换成标准的 JS 代码。

react中把jsx转为标准js的方式

  • Create React App (CRA) 默认使用 Babel 来进行 JSX 语法的转换。CRA 内部配置Babel及其相关插件(如 @babel/preset-react)。
  • Next.js:默认使用 SWC,支持 JSX、TS、React 编译,无需额外配置。
  • Vite:使用插件 @vitejs/plugin-react-swc 来启用 SWC 支持 JSX。

13. react 组件设计思想? 如何提高代码复用性?

组件的定义:

  1. UI 层 JSX 结构
  2. 逻辑层
  3. 状态层,useContent / useState / redux / jotai / Zustand

组件的特点:

  1. 独立性:封装自己的结构、样式、行为和状态。
  2. 可复用性:一个组件可以在多个地方重复使用,减少重复代码。
  3. 可组合性:组件之间可以嵌套、组合,形成更复杂的 UI。
  4. 状态与 UI 分离:状态变化自动驱动 UI 更新

如何提高代码复用性?

  1. 组件单一职责原则:一个组件只做一件事。
  2. UI 组件尽量无状态(stateless),由父组件或状态管理工具控制。
    • 通过 props 传递数据和行为,而不是在组件内部写死内容。
    • 状态逻辑交给 Hook 或状态管理工具(如 Redux、Zustand)
  3. 支持 children 和 render props。
  4. 使用自定义 Hook 复用逻辑 和 高阶组件(HOC)复用逻辑。

14. react 的“组件即函数”的理念? 类组件和函数组件本质区别?

组件即函数(Component as a Function)指的是:一个组件本质上就是一个JS函数。

  • 这个函数组件接收 props(输入)
  • 返回一个描述 UI 的 JSX(输出)
  • React 会根据这个输出更新 DOM

函数式编程思想

  • 纯函数:相同的输入,应返回相同的输出,且无副作用。
  • 不可变性:任何输入都会返回新值,不会修改原值。
  • 高阶函数:参数支持函数,也可以返回函数和值,如:函数柯里化。
  • 函数组合:通过组合多个小函数来构建复杂逻辑。

类组件和函数组件本质区别?

本质JavaScript 类(Class)JavaScript 函数
状态管理使用 this.state + setState使用 useState Hook
生命周期依赖类生命周期方法(如 componentDidMount使用 useEffect Hook 模拟生命周期
this 的绑定需要手动绑定 this无 this,更简洁
组件实例有实例(this),可访问内部状态和方法无实例,函数每次渲染都是独立的
性能优化使用 React.memoshouldComponentUpdate使用 React.memo 或 useCallbackuseMemo

15. react 严格模式有哪些弊端?

  • 生命周期的双重调用:严格模式下会故意触发某些生命周期方法两次,这是为了帮助检测那些不应该依赖于单次调用的副作用。