解析 `workInProgress` 与 `current` 树:双缓存技术(Double Buffering)如何保证 UI 更新的原子性?

各位同仁,下午好!

今天,我们将深入探讨一个在现代用户界面开发中至关重要的概念:如何利用“双缓存技术”——具体到我们今天的主题,便是通过 workInProgresscurrent 这两棵树——来确保UI更新的原子性。这不仅仅是一个理论上的优雅设计,更是许多高性能UI框架,特别是React Fiber架构,能够提供流畅、无撕裂用户体验的基石。

1. UI更新的挑战与原子性的需求

在复杂的交互式应用中,UI状态的更新是常态。用户点击按钮、数据从服务器返回、动画正在进行,这些操作都可能导致UI发生变化。然而,UI更新并非总是简单的“替换”操作。它可能涉及:

  • 多步操作: 一个完整的UI变化可能需要修改多个DOM节点、更新样式、执行动画等。
  • 依赖关系: 某个组件的渲染可能依赖于另一个组件的状态。
  • 性能考量: 频繁或大规模的DOM操作开销巨大,可能导致UI卡顿(jank)。
  • 用户体验: 用户不应看到UI处于一种“半成品”或不一致的状态。

想象一下,如果我们在更新UI时,用户恰好在中间某个阶段看到了屏幕。部分内容已经更新,而另一部分还停留在旧状态,或者正在经历复杂的计算。这会导致“UI撕裂”(UI Tearing)或“视觉跳动”(Flickering),严重损害用户体验。

因此,我们需要“原子性”的UI更新。原子性意味着一个操作要么完全成功并应用所有变更,要么完全失败并回滚到之前的状态,绝不会出现中间状态。对于UI更新而言,这意味着用户看到的永远是一个完整、一致的UI状态,无论这个状态是旧的还是新的。

2. 双缓存技术:通用原理

双缓存(Double Buffering)并非UI框架独有,它是一个在计算机图形学、操作系统甚至网络通信中广泛使用的技术模式。其核心思想是:

  1. 两个缓冲区: 维护两个独立的存储区域(缓冲区)。
  2. 一个可见,一个操作: 在任何时刻,只有一个缓冲区是“活跃”的,其内容被用户(或系统)读取或显示。另一个缓冲区则在后台进行内容的生成、修改或准备。
  3. 原子性切换: 当后台缓冲区的内容准备完毕并达到一个完整、一致的状态时,系统会以一个极快的、通常是硬件级别的操作,将这两个缓冲区的角色互换。原先的后台缓冲区变为活跃区,而原先的活跃区则变为后台区,等待下一次操作。

通过这种方式,用户永远不会看到后台缓冲区内容生成或修改的过程。他们看到的总是前台缓冲区的一个完整快照,然后在一瞬间切换到另一个完整的快照。这极大地减少了视觉上的不一致和撕裂。

特性 缓冲区 A (当前) 缓冲区 B (工作区)
角色 当前显示给用户的内容,只读。 正在构建或修改的新内容,可写。
可见性 可见 不可见
状态 稳定、已提交 可能不稳定、正在变化、未提交
更新 仅在交换时更新,否则保持不变 随时可更新、可中断、可丢弃
原子性 保证用户看到的是一个完整的“帧” 内部操作不影响外部感知

3. currentworkInProgress 树:UI的双缓存

现在,我们将双缓存的思想应用到UI树的更新上。在许多现代UI框架中,UI被抽象为一棵树形结构,这棵树描述了组件之间的父子关系、属性和状态。例如,在React中,这棵树就是Fiber树或虚拟DOM树。

为了实现原子性的UI更新,这些框架通常会维护两棵逻辑上的UI树:

  1. current 树 (当前树):

    • 代表了当前用户在屏幕上实际看到的、已经渲染并提交的UI状态。
    • 它是稳定的、只读的,任何时候都应指向一个完整的、一致的UI快照。
    • 外部世界(如用户事件监听、DOM查询)在大多数情况下都应与这棵树进行交互。
  2. workInProgress 树 (工作进行中树):

    • 代表了正在后台构建、计算或更新的新UI状态。
    • 它是可变的、可写的,是所有新渲染逻辑发生的地方。
    • 在更新过程中,这棵树可能处于不完整或不一致的状态,但这些状态对用户是不可见的。
    • 所有的计算、diffing、副作用(如React中的useEffectuseLayoutEffect的创建)都首先在这棵树上进行规划。

这两棵树通过一个巧妙的机制连接起来,通常是通过根节点上的一个指针,以及每个节点上的一个“交替”(alternate)指针。

4. React Fiber 架构中的 currentworkInProgress

React Fiber是React 16及以后版本中使用的核心协调(Reconciliation)引擎。它正是双缓存机制在UI树更新中的一个典范应用。理解Fiber如何使用currentworkInProgress,是理解React并发模式和性能优化的关键。

4.1 Fiber节点结构

在React Fiber中,每个UI元素(组件、DOM元素等)都被抽象为一个Fiber节点。一个Fiber节点包含了其类型、属性、状态、子节点、父节点等信息。为了支持双缓存,每个Fiber节点还包含一个至关重要的字段:alternate

// 概念性的Fiber节点结构
class FiberNode {
  constructor(tag, pendingProps, key) {
    this.tag = tag; // 元素类型(如函数组件、类组件、HostComponent等)
    this.key = key; // React key
    this.elementType = null; // 原始的组件类型
    this.type = null; // 实际的组件类型(如经过Memo处理后的)
    this.stateNode = null; // 实例化的组件或DOM节点

    this.return = null; // 指向父Fiber
    this.child = null; // 指向第一个子Fiber
    this.sibling = null; // 指向下一个兄弟Fiber

    this.pendingProps = pendingProps; // 待处理的props
    this.memoizedProps = null; // 已经处理过的props (current tree)
    this.updateQueue = null; // 状态更新队列

    this.memoizedState = null; // 已经处理过的state (current tree)

    this.alternate = null; // 指向另一个Fiber树中对应的Fiber节点!这是关键!

    // 副作用相关
    this.flags = NoFlags; // 描述该Fiber需要执行的副作用(如更新、插入、删除)
    this.subtreeFlags = NoFlags; // 子树的副作用
    this.deletions = null; // 待删除的子Fiber
  }
}

alternate 字段是连接 current 树和 workInProgress 树的桥梁。对于 current 树中的一个Fiber节点,其 alternate 指向 workInProgress 树中对应的节点;反之亦然。

4.2 根节点 (FiberRootNode)

整个React应用的入口是一个FiberRootNode。这个根节点不代表任何UI元素,而是管理整个Fiber树的生命周期。它有一个关键的属性:

class FiberRootNode {
  constructor(containerInfo, tag) {
    this.containerInfo = containerInfo; // 宿主容器(如DOM元素)
    this.tag = tag; // 根节点的类型
    this.current = null; // 指向当前渲染在屏幕上的Fiber树的根Fiber
    this.finishedWork = null; // 指向已完成的workInProgress树的根Fiber,等待提交
    // ... 其他调度相关属性
  }
}

fiberRoot.current 始终指向当前屏幕上显示的Fiber树的根Fiber节点。这是整个双缓存机制的入口点。

5. UI更新的生命周期:从 currentworkInProgress 再到 current

一个React组件的更新请求(如setStateforceUpdateReactDOM.render)会触发一个复杂的协调过程。这个过程可以被清晰地划分为两个主要阶段:渲染/协调阶段 (Render/Reconciliation Phase)提交阶段 (Commit Phase)

5.1 渲染/协调阶段 (Render/Reconciliation Phase) – 构建 workInProgress

当有更新请求到来时,React会启动一个新的渲染周期。这个阶段的核心目标是构建一棵全新的 workInProgress 树,它代表了更新后的UI状态。

  1. 创建 workInProgress 树的根节点:

    • React会从 fiberRoot.current(即当前屏幕上的树的根Fiber)开始。
    • 它会为其创建一个 alternate 节点,这个节点就是 workInProgress 树的根。如果 fiberRoot.current.alternate 已经存在,就直接复用。
    • workInProgress 树的根节点的 alternate 指向 current 树的根节点。
  2. 深度优先遍历与协调 (Reconciliation):

    • React会从根节点开始,以深度优先的方式遍历 current 树。
    • 对于 current 树中的每一个Fiber节点,React会尝试为其创建或复用一个 workInProgress 节点。
    • 克隆或创建:
      • 如果 currentFiber.alternate 已经存在,React会克隆 currentFiber 的属性到 currentFiber.alternate,并将其作为 workInProgressFiber
      • 如果不存在,React会创建一个全新的 FiberNode 作为 workInProgressFiber
    • 连接 alternate workInProgressFiber.alternate 会指向 currentFiber,而 currentFiber.alternate 会指向 workInProgressFiber。这样,两棵树的对应节点就通过 alternate 属性相互关联起来。
    • 协调子节点 (Diffing): 在处理完当前节点后,React会根据新的propsstate,以及组件的render方法返回的元素,与currentFiber的子节点进行比较(diffing)。
      • 如果子节点类型相同且key相同,则复用currentFiber的子节点的alternate作为workInProgressFiber的子节点,并继续向下处理。
      • 如果子节点类型或key不同,则创建新的FiberNode
      • 这个过程会找出需要插入、更新、移动或删除的节点,并在 workInProgressFiber 上标记相应的 flags(副作用标记)。
  3. 副作用收集:

    • 在遍历过程中,组件的render方法会被调用,Hooks(如useStateuseEffect)会被执行。
    • 所有需要执行的副作用(如DOM操作、生命周期方法、useEffect回调)都不会立即执行,而是被标记在 workInProgressFiber 上。
    • 这些带有副作用的 workInProgressFiber 节点会被链式连接起来,形成一个“副作用列表”(effectList),存储在根 FiberRootNodefinishedWork 上。
  4. 可中断性:

    • Fiber架构的一大亮点是其可中断性。渲染/协调阶段可能被更高优先级的任务打断。
    • 如果被打断,workInProgress 树可以被暂停,甚至被完全丢弃,而 current 树(屏幕上的UI)则完全不受影响。当调度器再次分配时间片时,可以从中断的地方继续,或者重新开始。
阶段特性 描述 关键概念
起始 收到更新请求,从 fiberRoot.current 开始
创建 current 树中的每个节点创建或复用对应的 workInProgress 节点 alternate 指针,克隆现有Fiber,创建新Fiber
协调 比较新旧 props/staterender 结果,找出差异 Diffing算法,更新队列,useStateuseReducer
副作用标记 将所有需要进行的DOM操作、生命周期方法、useEffect等标记在 workInProgress 节点上 flags 属性,副作用列表 (effectList)
状态 workInProgress 树可能不完整,不一致,但 current 树始终保持稳定 可中断性,暂停与恢复
输出 一棵完整的 workInProgress 树,以及一个包含所有副作用的 effectList fiberRoot.finishedWork 指向 workInProgress 树的根节点,effectList 存储待执行的副作用列表。

5.2 提交阶段 (Commit Phase) – 切换 currentworkInProgress

workInProgress 树完全构建完毕,并且所有副作用都被收集到 effectList 中时,协调阶段结束。接下来进入提交阶段。这个阶段是同步且不可中断的,因为它将 workInProgress 树的变更应用到实际的宿主环境(如DOM),并执行副作用。

提交阶段通常分为三个子阶段:

  1. beforeMutation(变更前):

    • 在这个阶段,React会遍历 effectList,执行一些在DOM变更之前需要完成的副作用。
    • 例如,执行 getSnapshotBeforeUpdate 生命周期方法,收集DOM布局信息。
    • 执行 useLayoutEffect 的回调函数。
  2. mutation(变更):

    • 这是真正执行DOM操作的阶段。React再次遍历 effectList
    • 根据 flags 标记,对实际的DOM进行插入、更新、删除等操作。
    • 例如,将新的DOM节点插入到页面中,更新现有DOM节点的属性,移除不再需要的DOM节点。
    • 关键的原子性切换发生在这里: 在DOM操作完成之后,或者在进行DOM操作之前,fiberRoot.current 指针会被原子性地更新,指向刚刚构建完成的 workInProgress 树的根节点。
    // 简化后的提交过程中的关键步骤
    function commitRoot(root) {
      const finishedWork = root.finishedWork; // 已经完成的workInProgress树的根节点
    
      // ... 执行 beforeMutation 阶段的副作用 (e.g., useLayoutEffect)
    
      // ** 核心的原子性切换! **
      // 将 current 树更新为刚刚构建好的 workInProgress 树
      root.current = finishedWork;
    
      // ... 执行 mutation 阶段的副作用 (DOM 更新、删除、插入)
      //    此时,root.current 已经指向新的树,后续的 DOM 操作是基于新树的结构
      //    但这里的 DOM 操作仍然是根据 finishedWork 上的 flags 来执行的
    
      // ... 执行 layout 阶段的副作用 (e.g., componentDidMount/Update)
    
      // ... 执行 passive 阶段的副作用 (e.g., useEffect)
    
      root.finishedWork = null; // 清除待提交的树
    }

    这个 root.current = finishedWork; 的赋值操作是原子性的。它只修改一个指针,这个操作在现代CPU上是纳秒级别的,几乎可以认为是瞬间完成的。这意味着,在赋值之前,current 树是旧的,赋值之后,current 树是新的。没有中间状态。

  3. layout(布局):

    • 在DOM变更完成后,React会再次遍历 effectList,执行一些需要访问已更新DOM的副作用。
    • 例如,执行 componentDidMountcomponentDidUpdate 生命周期方法。
    • 通常,这个阶段也是同步的。
  4. passive(被动):

    • 这是在浏览器绘制之后异步执行的阶段。
    • 例如,执行 useEffect 的清理函数和回调函数。
    • 这个阶段是异步的,不会阻塞浏览器绘制,因此对用户体验影响最小。
阶段特性 描述 关键操作 原子性保证
beforeMutation 在DOM变更前执行副作用,收集快照信息 getSnapshotBeforeUpdateuseLayoutEffect 回调 准备阶段,不影响可见UI
mutation 执行DOM变更,并原子性地切换 root.current 指针 root.current = finishedWork,DOM插入/更新/删除 root.current 指针的原子性切换,保证始终引用完整树
layout 在DOM变更后执行副作用,如访问DOM componentDidMount/UpdateuseLayoutEffect 清理 基于已更新的稳定DOM和current
passive 异步执行副作用,不阻塞渲染 useEffect 回调及清理 异步执行,不影响同步渲染原子性
最终状态 current 树指向新的UI状态,workInProgress 树被清空,等待下一次更新开始。 fiberRoot.finishedWork = null

6. 原子性更新的实现机制与保证

通过 currentworkInProgress 这两棵树的双缓存机制,React Fiber能够提供强大的原子性更新保证:

  1. 可见UI的始终一致性:

    • fiberRoot.current 始终指向一棵完全构建、完全一致的Fiber树。
    • 用户看到的UI(由 current 树描述)永远不会处于一个中间的、不完整的状态。
    • 所有复杂的计算、组件渲染、diffing 都发生在 workInProgress 树上,这个过程对用户是不可见的。
  2. “撕裂”现象的消除:

    • 由于 root.current 的切换是原子性的(一个指针赋值操作),从旧的UI状态到新的UI状态的转变发生在极短的时间内。
    • 这避免了用户看到UI元素在更新过程中出现部分新、部分旧的“撕裂”现象。就像电影胶片一帧一帧地播放,每帧都是完整的。
  3. 容错性与韧性:

    • 如果在渲染/协调 workInProgress 树的过程中发生错误(例如,组件渲染抛出异常),或者因为优先级原因被中断,workInProgress 树可以被安全地丢弃。
    • current 树不受影响,用户看到的UI仍然是稳定的,不会因此而崩溃或显示不正确的状态。这为错误边界(Error Boundaries)等机制提供了基础。
  4. 并发渲染的基础:

    • 双缓存是React并发模式能够实现的关键。因为 workInProgress 树的构建是可中断的,React可以在多个更新之间切换,甚至暂停一个低优先级的更新,去处理一个高优先级的更新,而不会影响到当前用户看到的UI。
    • 不同的更新可以在后台并行地构建各自的 workInProgress 树(虽然在单线程JS中是时间切片模拟的并行),一旦准备就绪,就可以提交。

7. 概念性代码示例

为了更好地理解,我们用伪代码来模拟一下这个过程。

// 假设的Fiber节点结构
class Fiber {
  constructor(type, props, alternate = null) {
    this.type = type;
    this.props = props;
    this.children = []; // 存储子Fiber
    this.domElement = null; // 对应的真实DOM元素
    this.alternate = alternate; // 指向另一棵树中对应的Fiber
    this.flags = 0; // 副作用标记 (UPDATE, PLACEMENT, DELETION)
  }
}

// 假设的FiberRootNode
class FiberRoot {
  constructor(container) {
    this.container = container; // 宿主DOM容器
    this.current = null; // 指向当前渲染在屏幕上的Fiber树的根Fiber
    this.finishedWork = null; // 指向已完成的workInProgress树的根Fiber
    this.effectList = []; // 待执行的副作用列表
  }
}

// 模拟创建或复用 workInProgress Fiber
function createWorkInProgress(currentFiber) {
  if (currentFiber && currentFiber.alternate) {
    // 如果 alternate 存在,复用它
    const workInProgressFiber = currentFiber.alternate;
    workInProgressFiber.props = currentFiber.props; // 更新props
    workInProgressFiber.children = []; // 清空子节点,等待重新协调
    workInProgressFiber.flags = 0; // 重置副作用标记
    return workInProgressFiber;
  } else {
    // 否则创建一个新的 Fiber
    const newFiber = new Fiber(currentFiber.type, currentFiber.props);
    if (currentFiber) {
      newFiber.alternate = currentFiber;
      currentFiber.alternate = newFiber;
    }
    return newFiber;
  }
}

// 模拟协调子节点
function reconcileChildren(workInProgressFiber, newChildren) {
  // 简化的协调逻辑,实际React会更复杂
  workInProgressFiber.children = newChildren.map(child => {
    // 假设这里会进行diffing,并可能创建新的Fiber或复用旧的Fiber
    // 为了简化,我们直接创建新的 Fiber 节点作为 workInProgress 的子节点
    const childFiber = new Fiber(child.type, child.props);
    childFiber.flags |= PLACEMENT; // 假设所有新创建的都需要插入
    workInProgressFiber.effectList.push(childFiber); // 收集副作用
    return childFiber;
  });
}

const UPDATE = 1;
const PLACEMENT = 2;
const DELETION = 4;

// 模拟渲染/协调阶段
function performUnitOfWork(workInProgressFiber, currentFiber) {
  // 1. 根据组件类型,调用 render 方法或处理宿主元素
  let newChildren = [];
  if (workInProgressFiber.type === 'div' || workInProgressFiber.type === 'span') {
    // 宿主组件,直接处理 props
    if (currentFiber && workInProgressFiber.props !== currentFiber.props) {
      workInProgressFiber.flags |= UPDATE;
    }
    // 假设从 props 中提取 children
    newChildren = workInProgressFiber.props.children || [];
  } else {
    // 假设是函数组件,调用其 render 逻辑
    const element = workInProgressFiber.type(workInProgressFiber.props);
    newChildren = element.children || [];
  }

  // 2. 协调子节点
  reconcileChildren(workInProgressFiber, newChildren);

  // 3. 返回下一个要处理的 Fiber
  // 深度优先遍历的简化逻辑
  if (workInProgressFiber.children.length > 0) {
    return workInProgressFiber.children[0];
  }
  let sibling = workInProgressFiber.sibling;
  while (!sibling && workInProgressFiber.return) {
    workInProgressFiber = workInProgressFiber.return;
    sibling = workInProgressFiber.sibling;
  }
  return sibling;
}

// 模拟提交阶段
function commitWork(fiberRoot, finishedWork) {
  // 1. 执行 beforeMutation 副作用 (useLayoutEffect等)
  // ...

  // ** 2. 原子性切换 `current` 树 **
  fiberRoot.current = finishedWork; // 这一步是关键!

  // 3. 遍历 effectList,执行 DOM 变更 (mutation)
  let currentEffect = fiberRoot.effectList[0]; // 简化,实际是链表
  while (currentEffect) {
    const fiber = currentEffect;
    if (fiber.flags & PLACEMENT) {
      // 插入DOM
      const parentDom = fiber.return.domElement;
      const childDom = document.createElement(fiber.type);
      childDom.textContent = fiber.props.children; // 简化内容
      parentDom.appendChild(childDom);
      fiber.domElement = childDom;
    } else if (fiber.flags & UPDATE) {
      // 更新DOM
      const dom = fiber.domElement;
      // 假设更新属性
      dom.textContent = fiber.props.children;
    }
    // ... 处理 DELETION
    currentEffect = fiberRoot.effectList.shift(); // 移除已处理的effect
  }

  // 4. 执行 layout 副作用 (componentDidMount/Update等)
  // ...

  // 5. 执行 passive 副作用 (useEffect等)
  // ...

  fiberRoot.finishedWork = null;
  fiberRoot.effectList = [];
}

// 启动一个更新循环
let nextUnitOfWork = null;
let pendingWorkInProgressRoot = null;

function scheduleUpdate(fiberRoot, newRootElement) {
  // 1. 创建 workInProgress 树的根节点
  const currentRootFiber = fiberRoot.current;
  const workInProgressRootFiber = createWorkInProgress(currentRootFiber);
  workInProgressRootFiber.props = newRootElement.props; // 设置新的props
  workInProgressRootFiber.type = newRootElement.type;

  pendingWorkInProgressRoot = workInProgressRootFiber;
  nextUnitOfWork = workInProgressRootFiber;

  // 启动调度循环
  requestIdleCallback(workLoop);
}

function workLoop(deadline) {
  while (nextUnitOfWork && deadline.timeRemaining() > 1) {
    // 模拟从 workInProgressRootFiber 开始构建整棵树
    const currentFiber = nextUnitOfWork.alternate; // 获取 current 树中的对应节点
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork, currentFiber);
  }

  if (!nextUnitOfWork && pendingWorkInProgressRoot) {
    // 所有 work 都完成了,准备提交
    fiberRoot.finishedWork = pendingWorkInProgressRoot;
    commitWork(fiberRoot, fiberRoot.finishedWork);
    pendingWorkInProgressRoot = null;
  } else if (nextUnitOfWork) {
    // 还有工作未完成,请求下一次空闲时间
    requestIdleCallback(workLoop);
  }
}

// --- 实际使用示例 ---
const container = document.getElementById('root');
const fiberRoot = new FiberRoot(container);

// 首次渲染
scheduleUpdate(fiberRoot, {
  type: 'div',
  props: {
    children: [{
      type: 'span',
      props: {
        children: 'Hello initial'
      }
    }]
  }
});

// 模拟一段时间后更新
setTimeout(() => {
  scheduleUpdate(fiberRoot, {
    type: 'div',
    props: {
      children: [{
        type: 'span',
        props: {
          children: 'Hello updated!'
        }
      }, {
        type: 'span',
        props: {
          children: ' New Span!'
        }
      }]
    }
  });
}, 2000);

这个伪代码展示了:

  • Fiber 节点如何通过 alternate 相互链接。
  • FiberRoot 如何通过 current 指向活动的UI树。
  • createWorkInProgress 如何克隆或创建 workInProgress 节点。
  • performUnitOfWork 如何模拟协调过程,构建 workInProgress 树并标记副作用。
  • commitWork 如何在 mutation 阶段原子性地更新 fiberRoot.current,然后执行DOM操作。

8. 收益与权衡

收益:

  • 原子性与一致性: 保证用户看到的UI始终处于一个完整、一致的状态,消除了UI撕裂。
  • 流畅的用户体验: 复杂的计算和DOM操作在后台进行,不会阻塞主线程,使得UI在更新过程中保持响应。
  • 更好的错误恢复: 如果后台计算失败,当前UI不受影响,提高了应用的健壮性。
  • 支持并发与优先级: 为React的并发模式和时间切片调度提供了基础,使得更智能的UI更新成为可能。
  • 分离关注点: 将“计算更新”与“应用更新”两个阶段清晰分离。

权衡:

  • 内存开销: 维护两棵完整的UI树(currentworkInProgress)意味着需要双倍的内存。尽管React会尝试复用节点,但对于大型应用来说,这仍然是一个需要考虑的因素。
  • 实现复杂度: 协调引擎的内部逻辑变得更加复杂,需要精心设计来管理两棵树的状态、alternate 指针、副作用列表等。
  • 调试挑战: 由于UI更新是分阶段进行的,并且涉及两棵树的交互,调试可能会变得更加复杂。

9. 现代UI框架的基石

双缓存技术,通过 currentworkInProgress 树的抽象,是现代高性能UI框架实现流畅、可靠用户体验的基石。它不仅解决了UI更新的原子性问题,更为并发渲染、时间切片、优先级调度等高级功能铺平了道路。理解这一机制,对于深入掌握React等框架的内部运作原理,以及开发高性能的复杂UI应用,都具有不可估量的价值。我们看到,一个看似简单的指针切换,背后却蕴含着对UI生命周期、性能和用户体验的深刻洞察。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注