Wsh's blog Wsh's blog
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)

wsh

热爱前端的程序媛
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)
  • react基础

  • react更新特性

  • react进阶

    • react 架构演变
    • react render阶段
    • react commit阶段
    • react diff
    • react 状态更新
    • react hook
    • Concurrent Mode
    • react
    • react进阶
    2022-04-22
    目录

    Concurrent Mode

    # 01. scheduler

    scheduler主要包括两个功能:

    • 时间切片

    • 优先级调度

    # 时间切片原理

    Scheduler的时间切片功能是通过task(宏任务)实现的。 常见的tast就是setTimout,但是有个task比setTimeout执行时机更靠前,MessageChannel (opens new window)允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据。

    所以Scheduler将需要被执行的回调函数作为MessageChannel的回调执行。如果当前宿主环境不支持MessageChannel,再使用setTimeout。

    requestHostCallback = function(cb) {
      if (_callback !== null) {
        // Protect against re-entrancy.
        setTimeout(requestHostCallback, 0, cb);
      } else {
        _callback = cb;
        setTimeout(_flushCallback, 0);
      }
    };
    

    在React的render阶段,开启Concurrent Mode时,每次遍历前,都会通过Scheduler提供的shouldYield方法判断是否需要中断遍历,使浏览器有时间渲染 是否中断的依据:每个任务的剩余时间是否用完。在Schdeduler中,为任务分配的初始剩余时间为5ms

    function workLoopConcurrent() {
      while (workInProgress !== null && !shouldYield()) {
        performUnitOfWork(workInProgress);
      }
    }
    

    通过fps动态调整分配给任务的可执行时间

    forceFrameRate = function(fps) {
        //fps: 每秒传输速率,不支持强速帧 
        if (fps < 0 || fps > 125) {
          return;
        }
        if (fps > 0) {
          yieldInterval = Math.floor(1000 / fps);
        } else {
          // 重置初始值
          yieldInterval = 5;
        }
    };
    
    

    # 优先级调度

    Scheduler是独立于React的包,所以它的优先级(存在5种优先级)也是独立于React的优先级的。对外暴露了一个方法 unstable_runWithPriority

    function unstable_runWithPriority(priorityLevel, eventHandler) {
      switch (priorityLevel) {
        case ImmediatePriority:
        case UserBlockingPriority:
        case NormalPriority:
        case LowPriority:
        case IdlePriority:
          break;
        default:
          priorityLevel = NormalPriority;
      }
    
      var previousPriorityLevel = currentPriorityLevel;
      currentPriorityLevel = priorityLevel;
    
      try {
        return eventHandler();
      } finally {
        currentPriorityLevel = previousPriorityLevel;
      }
    }
    

    不同优先级意味着不同时长的任务过期时间

    // Times out immediately
    var IMMEDIATE_PRIORITY_TIMEOUT = -1;
    // Eventually times out
    var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
    var NORMAL_PRIORITY_TIMEOUT = 5000;
    var LOW_PRIORITY_TIMEOUT = 10000;
    // Never times out
    var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
    
    
    var timeout;
    switch (priorityLevel) {
      case ImmediatePriority:
        timeout = IMMEDIATE_PRIORITY_TIMEOUT;
        break;
      case UserBlockingPriority:
        timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
        break;
      case IdlePriority:
        timeout = IDLE_PRIORITY_TIMEOUT;
        break;
      case LowPriority:
        timeout = LOW_PRIORITY_TIMEOUT;
        break;
      case NormalPriority:
      default:
        timeout = NORMAL_PRIORITY_TIMEOUT;
        break;
    }
    
    var expirationTime = startTime + timeout;
    

    如果一个任务的优先级是ImmediatePriority,对应IMMEDIATE_PRIORITY_TIMEOUT为-1,那么该任务的过期时间比当前时间还短,表示它已经过期了,需要立即被执行。

    # 不同优先级的排序任务

    Scueduler 存在两个队列

    • timerQueue:保存未就绪任务
    • taskQueue:保存已就绪任务 每当有新的未就绪的任务被注册,我们将其插入到timerQueue,并根据开始时间重新排序。 当timerQueue中有任务就绪(currentTime >= startTime),取出加入到taskQueue。 取出taskQueue中最早过期的任务并执行他。Scheduler使用小顶堆实现了优先级队列。

    小顶堆

    每个结点的值都小于或等于其左右孩子结点的值
    arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
    

    取出taskQueue最早过期任务:注册的回调函数执行后的返回值continuationCallback为function,会将continuationCallback作为当前任务的回调函数,如果不是funtion,则将当前被执行的任务清除taskQueue。

    const continuationCallback = callback(didUserCallbackTimeout);
    currentTime = getCurrentTime();
    if (typeof continuationCallback === 'function') {
      // continuationCallback是函数
      currentTask.callback = continuationCallback;
      markTaskYield(currentTask, currentTime);
    } else {
      if (enableProfiling) {
        markTaskCompleted(currentTask, currentTime);
        currentTask.isQueued = false;
      }
      if (currentTask === peek(taskQueue)) {
        // 将当前任务清除
        pop(taskQueue);
      }
    }
    advanceTimers(currentTime);
    

    # 02. lane模型

    lane模型使用31位的二进制表示31条赛道,位数越小的赛道优先级越高,某些相邻的赛道拥有相同优先级。

    优先级逐步降低.

    代表批处理的lanes: 同时占据好几个赛道

    export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
    export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;
    
    export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
    export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;
    
    export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
    const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;
    
    const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
    const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;
    
    export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000;
    export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;
    
    const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
    const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;
    
    const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
    
    export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;
    
    export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;
    
    const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;
    
    export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
    const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;
    
    export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;
    

    InputDiscreteLanes 是“用户交互”触发更新会拥有的优先级范围。

    DefaultLanes是“请求数据返回后触发更新”拥有的优先级范围。

    TransitionLanes是Suspense、useTransition、useDeferredValue拥有的优先级范围。

    # 03. 位与运算:

    • 前置知识:

    正数的原码,反码,补码不变 负数的反码是对其原码按位取反,符号位不变 补码是在其反码基础上+1

    例如:

    let lanes =  0b011 -> 00000011
       -lanes = -0b011 -> 10000011(原码) ->11111100(反码) -> 11111101(补码)
       lanes & -lanes = 00000011 &  11111101 -> 00000001 -> 1
    

    位合并位运算:

    export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
      return a | b;
    }
    

    lane存在交集:

    export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
      return (a & b) !== NoLane;
    }
    

    subSet是set的子集:

    export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
      return (set & subset) === subset;
    }
    

    将subset从set移除:

    export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
      return set & ~subset;
    }
    

    # 04. 异步可中断

    # batchedUpdates 批量更新(17版本)

    
                          wrappers (injected at creation time)
                                         +        +
                                         |        |
                       +-----------------|--------|--------------+
                       |                 v        |              |
                       |      +---------------+   |              |
                       |   +--|    wrapper1   |---|----+         |
                       |   |  +---------------+   v    |         |
                       |   |          +-------------+  |         |
                       |   |     +----|   wrapper2  |--------+   |
                       |   |     |    +-------------+  |     |   |
                       |   |     |                     |     |   |
                       |   v     v                     v     v   | wrapper
                       | +---+ +---+   +---------+   +---+ +---+ | invariants
    perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
    +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
                       | |   | |   |   |         |   |   | |   | |
                       | |   | |   |   |         |   |   | |   | |
                       | |   | |   |   |         |   |   | |   | |
                       | +---+ +---+   +---------+   +---+ +---+ |
                       |  initialize                    close    |
                       +-----------------------------------------+
    
    

    react的 batchUpdate是通过 transaction实现的。transaction对一个函数进行包装,让react 有机会在一个函数运行前后执行特定逻辑,从而完成整个batchUpdate流程的控制。

    transaction是给需要执行的函数封装了两个 wrapper,每个 wrapper 都有 initialize 和 close 方法。当一个 transaction 需要执行(perform)的时候,会先调用对应的 initialize 方法。同样的,当一个 transaction 执行完成后,会调用对应的 close 方法。

    在transaction的initialize阶段,一个updateQueue被创建。在transaction中调用setState方法时,状态不会立即应用,而是被推入到updateQueue中。函数执行结束进入到transactionclose阶段, updateQueue会被flush,此时新的状态会被应用到组件上并进行虚拟dom更新等工作。

    # 高优先级更新插队

    低优先级任务在执行中,一旦比他高的优先级任务及哪里,这个低优先级任务会被中断,执行高优先级任务,等高优先级任务执行完毕,低优先级任务会重新执行。

    当onClick调用setState时,意味着组件对应的fiber节点产生了一个更新。setState实际上是生成一个update对象,调用 enqueueSetState,将这个update对象连接到fiber节点的updateQueue链表中.

    Component.prototype.setState = function(partialState, callback) {
      this.updater.enqueueSetState(this, partialState, callback, 'setState');
    };
    

    enqueueSetState的目的就是创建update对象,将它加入到 fiber节点的update链表 updateQueue,然后发起调度。

    enqueueSetState(inst, payload, callback) {
        // 获取当前触发更新的fiber节点。inst是组件实例
        const fiber = getInstance(inst);
        // eventTime是当前触发更新的时间戳
        const eventTime = requestEventTime();
        const suspenseConfig = requestCurrentSuspenseConfig();
    
        // 获取本次update的优先级
        const lane = requestUpdateLane(fiber, suspenseConfig);
    
        // 创建update对象
        const update = createUpdate(eventTime, lane, suspenseConfig);
    
        // payload就是setState的参数,回调函数或者是对象的形式。
        // 处理更新时参与计算新状态的过程
        update.payload = payload;
    
        // 将update放入fiber的updateQueue
        enqueueUpdate(fiber, update);
    
        // 开始进行调度
        scheduleUpdateOnFiber(fiber, lane, eventTime);
      }
    
    • 计算优先级 requestUpdateLane

    事件触发时,合成事件机制调用scheduler中的runWithPriority函数,目的是以该交互事件对应的事件优先级去派发真正的事件流程。runWithPriority会将事件优先级转化为scheduler内部的优先级并记录下来。当调用requestUpdateLane计算lane的时候,会去获取scheduler中的优先级,以此作为lane计算的依据。(findUpdateLane)

    • 创建 update 对象
    • 将update放入updateQueue中
    • scheduleUpdateOnFiber 进行调度

    # 调度准备

    1. 检查是否有无限更新

    2. 从产生更新的节点开始一直向上循环到root,目的是要将fiber.lanes一直向上收集,收集到父级节点的childLanes中,childLanes是识别子数是否更新的关键。如果fiber.lanes不为空,说明该fiber节点有更新。fiber.childLanes 判断当前子树是否有更新的依据,若有更新,继续向下构建,否则直接复用已有的fiber树,就不往下循环了。

    3. 在root上标记更新,将update的lane放到root.pendingLanes中,每次渲染的优先级基准:renderLanes是来自于root.pendingLanes中最紧急的那一部分lanes。

    function scheduleUpdateOnFiber(
      fiber: Fiber,
      lane: Lane,
      eventTime: number,
    ) {
      // 第一步,检查是否有无限更新
      checkForNestedUpdates();
    
      ...
      // 第二步,向上收集fiber.childLanes
      const root = markUpdateLaneFromFiberToRoot(fiber, lane);
    
      ...
    
      // 第三步,在root上标记更新,将update的lane放到root.pendingLanes
      markRootUpdated(root, lane, eventTime);
    
      ...
    
      // 根据Scheduler的优先级获取到对应的React优先级
      const priorityLevel = getCurrentPriorityLevel();
    
      if (lane === SyncLane) {
        // 本次更新是同步的,例如传统的同步渲染模式
        if (
          (executionContext & LegacyUnbatchedContext) !== NoContext &&
          (executionContext & (RenderContext | CommitContext)) === NoContext
        ) {
          // 如果是本次更新是同步的,并且当前还未渲染,意味着主线程空闲,并没有React的更新任务在执行,那么调用performSyncWorkOnRoot开始执行同步任务
          // ...
          performSyncWorkOnRoot(root);
        } else {
          // 如果是本次更新是同步的,不过当前有React更新任务正在进行,而且因为无法打断,所以调用ensureRootIsScheduled 目的是去复用已经在更新的任务,让这个已有的任务把这次更新顺便做了
          ensureRootIsScheduled(root, eventTime);
          ...
        }
      } else {
    
        ...
    
        // Schedule other updates after in case the callback is sync.
        // 如果是更新是异步的,调用ensureRootIsScheduled去进入异步调度
        ensureRootIsScheduled(root, eventTime);
        schedulePendingInteractions(root, lane);
      }
    
      ...
    }
    

    scheduleUpdateOnFiber最终会调用ensureRootIsScheduled来调度。 一个update的产生最终会使React在内存中根据现有的fiber树构建一棵新的fiber树,新的state的计算、diff操作、以及一些生命周期的调用,都会在这个构建过程中进行。这个整体的构建工作被称为render阶段,这个render阶段整体就是一个完整的React更新任务,更新任务可以看作执行一个函数,这个函数在concurrent模式下就是performConcurrentWorkOnRoot,更新任务的调度可以看成是这个函数被scheduler按照任务优先级安排它何时执行。

    # react任务的本质

    一个update的产生最终会使react在内存中 根据现有的fiber树构建一颗新的fiber树,新的state计算,diff操作,还有一些声明周期的调用,都会在这个构建过程中进行。也就是我们所说的render阶段。 render阶段就是一个完成的react 更新任务。 在concurrent模式下,就是performConcurrentWorkOnRoot,更新任务的调度我们可以理解为这个函数被scheduler 按照任务优先级安排它何时执行。

    每当有新的任务来的时候,会被挂载到root节点的callbackNode属性上,表示当前有任务被调度了,另外将任务优先级存储到root的callbackPriority上

    # 任务调度协调 - ensureRootIsScheduled

    主要职能:

    • 获取 root.callbackNode 即旧任务
    • 检查任务是否过期,将过期任务放入root.expriedLanes,目的是让过期任务能够以同步优先级去进行调度(立即执行)
    • 获取renderLanes(优先从root.expriedLanes获取),如果renderLanes是空的,说明不需要调度,直接return
    • 获取本次优先级,即新的优先级,newCallbackPriority

    协调调度过程:

    • 首先确认是否发起调度,通过对比新旧任务优先级是否相等

      • 相等:无需调度,直接复用旧任务,让旧任务在处理更新的时候连带新任务一起做
      • 不相等:说明新任务的优先级高于旧任务, 高优先级任务插队,需要把旧任务取消

      这是因为每次调度去获取任务优先级(renderLanes)的时候,都只获取root.pendingLanes中最紧急的那部分lanes对应的优先级,低优先级的update持有的lane对应的优先级是无法被获取到的。通过这种办法,可以将来自同一事件中的多个更新收敛到一个任务中去执行,通俗理解就是同一个事件触发的多次更新的优先级是一样的,没必要发起多次任务调度。例如在一个事件中多次调用setState,只引起一次调度,后续调度与第一次调度优先级一致,直接复用了。

    • 发起调度:查看新任务的优先级

      • 同步优先级(legacy 模式):调用 scheduleSyncCallback去同步执行任务
      • 同步批量执行(blocking 模式):调用 scheduleCallback 以同步任务优先级(立即调用)调度
      • 属于conCurrent优先级(conCurrent 模式):调用scheduleCallback将任务以上面获取到的新任务优先级去加入调度。
    function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
      // 获取旧任务
      const existingCallbackNode = root.callbackNode;
    
      // 记录任务的过期时间,检查是否有过期任务,有则立即将它放到root.expiredLanes,
      // 便于接下来将这个任务以同步模式立即调度
      markStarvedLanesAsExpired(root, currentTime);
    
      // 获取renderLanes
      const nextLanes = getNextLanes(
        root,
        root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
      );
    
      // 获取renderLanes对应的任务优先级
      const newCallbackPriority = returnNextLanesPriority();
    
      if (nextLanes === NoLanes) {
        // 如果渲染优先级为空,则不需要调度
        if (existingCallbackNode !== null) {
          cancelCallback(existingCallbackNode);
          root.callbackNode = null;
          root.callbackPriority = NoLanePriority;
        }
        return;
      }
    
      // 如果存在旧任务,那么看一下能否复用
      if (existingCallbackNode !== null) {
    
        // 获取旧任务的优先级
        const existingCallbackPriority = root.callbackPriority;
    
        // 如果新旧任务的优先级相同,则无需调度
        if (existingCallbackPriority === newCallbackPriority) {
          return;
        }
        // 代码执行到这里说明新任务的优先级高于旧任务的优先级
        // 取消掉旧任务,实现高优先级任务插队
        cancelCallback(existingCallbackNode);
      }
    
      // 调度一个新任务
      let newCallbackNode;
      if (newCallbackPriority === SyncLanePriority) {
    
        // 若新任务的优先级为同步优先级,则同步调度,传统的同步渲染和过期任务会走这里
        newCallbackNode = scheduleSyncCallback(
          performSyncWorkOnRoot.bind(null, root),
        );
      } else if (newCallbackPriority === SyncBatchedLanePriority) {
    
        // 同步模式到concurrent模式的过渡模式:blocking模式会走这里
        newCallbackNode = scheduleCallback(
          ImmediateSchedulerPriority,
          performSyncWorkOnRoot.bind(null, root),
        );
      } else {
        // concurrent模式的渲染会走这里
    
        // 根据任务优先级获取Scheduler的调度优先级
        const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
          newCallbackPriority,
        );
    
        // 计算出调度优先级之后,开始让Scheduler调度React的更新任务
        newCallbackNode = scheduleCallback(
          schedulerPriorityLevel,
          performConcurrentWorkOnRoot.bind(null, root),
        );
      }
    
      // 更新root上的任务优先级和任务,以便下次发起调度时候可以获取到
      root.callbackPriority = newCallbackPriority;
      root.callbackNode = newCallbackNode;
    }
    

    ensureRootIsScheduled 在任务调度层面整合了高优先级任务的插队和任务饥饿问题的关键逻辑。

    饥饿问题:是指低优先级任务始终被高优先级任务打断,导致没有时间执行。

    高优先级任务插队,低优先级任务重做的整个过程共有四个关键点:

    • ensureRootIsScheduled取消已有的低优先级更新任务,重新调度一个任务去做高优先级更新,并以root.pendingLanes中最重要的那部分lanes作为渲染优先级
    • 执行更新任务时跳过updateQueue中的低优先级update,并将它的lane标记到fiber.lanes中。
    • fiber节点的complete阶段收集fiber.lanes到父级fiber的childLanes,一直到root。
    • commit阶段将所有root.childLanes连同root.lanes一并赋值给root.pendingLanes。
    • commit阶段的最后重新发起调度,(ensureRootIsScheduled实现)重新走一遍调度流程,确保低优先级任务被执行。

    # 05. Suspense实现

    处理流程:

    • Suspense 让子组件在渲染之前进行等待,并在等待时显示 fallback 的内容
    • Suspense 内的组件子树比组件树的其他部分拥有更低的优先级

    执行流程:

    • 在 render 函数中可以使用异步请求数据
    • react 会从我们的缓存中读取,如果缓存命中,直接进行 render,如果没有缓存,会抛出一个 promise 异常。当 promise 完成后,react 会重新进行 render,把数据展示出来,完全同步写法,没有任何异步 callback。

    # 01简易版代码实现:

    1. 子组件没有加载完成时,会抛出一个 promise 异常
    2. 监听 promise,状态变更后,更新 state,触发组件更新,重新渲染子组件
    3. 展示子组件内容
    export default class Suspense extends React.Component {
        state = {
            loading: false,
        };
    
        componentDidCatch(error) {
            if (error && typeof error.then === "function") {
                error.then(() => {
                    this.setState({ loading: true });
                });
                this.setState({ loading: false });
            }
        }
    
        render() {
            const { fallback, children } = this.props;
            const { loading } = this.state;
            return loading ? fallback : children;
        }
    }
    

    # 02简易版代码实现:

    问题是我们在编写组件的时候,不能直接使用async await, 结合suspense原理,封装一个promise,请求中,我们将 promise 作为异常抛出,请求完成展示结果。

    定义一个WrapPromise 函数:

    function WrapPromise (promise) {
      let status = "pending";
        let result;
        let suspender = promise.then(
            (res) => {
                status = "success";
                result = res;
            },
            (error) => {
                status = "error";
                result = error;
            }
        );
      return {
            exec() {
                if (status === "pending") {
                    throw suspender;
                } else if (status === "error") {
                    throw result;
                } else if (status === "success") {
                    return result;
                }
            },
      };
    }
    

    # SuspenseList

    # 三个属性
    • revealOrder: 子 Suspense 的加载顺序
      • forwards: 从前向后展示,无论请求的速度快慢都会等前面的先展示
      • backwards: 从后向前展示,无论请求的速度快慢都会等后面的先展示
      • together: 所有的 Suspense 都准备好之后同时显示
    • tail: 指定如何显示 SuspenseList 中未准备好的 Suspense
      • 不设置:默认加载所有 Suspense 对应的 fallback
      • collapsed:仅展示列表中下一个 Suspense 的 fallback
      • hidden: 未准备好的项目不限时任何信息
    • children: 子元素
      • 子元素可以是任意 React 元素

    当子元素中包含非 Suspense 组件时,且未设置 tail 属性,那么此时所有的 Suspense 元素必定是同时加载,设置 revealOrder 属性也无效。当设置 tail 属性后,无论是 collapsed 还是 hidden,revealOrder 属性即可生效 子元素中多个 Suspense 不会相互阻塞。

    SuspenseList仅对最近的组件和其下方的Suspense组件SuspenseList进行操作。它不会搜索比一层更深的边界。但是,可以将多个SuspenseList组件相互嵌套以构建网格。

    <SuspenseList revealOrder="forwards" tail="collapsed">
      <Suspense fallback={<div>Loading...</div>}>
        <User id={1} />
      </Suspense>
      <Suspense fallback={<div>Loading...</div>}>
        <User id={3} />
      </Suspense>
      <Suspense fallback={<div>Loading...</div>}>
        <User id={5} />
      </Suspense>
    </SuspenseList>
    
    #react
    react hook

    ← react hook

    最近更新
    01
    组件通信方式
    01-07
    02
    UIAbility
    01-07
    03
    ATKTS
    01-06
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025 Wsh | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式