RN 的事件调度 RuntimeScheduler

本文所有代码如果没有特别标注的话,默认用的都是 v0.76.0 的 RN 代码

为什么需要 RuntimeScheduler

在新架构的综述文章中我们聊过旧版本 RN 典型的渲染流程是:

js 复制代码
JS 接收 Native 的消息
  ↓
JS 执行更新逻辑
  ↓
JS 通过 Bridge 将更新指令发回 Native
  ↓
Native 触发渲染流程

现在我们尝试用线程的视角重新看一下这个流程:

js 复制代码
[JS thread] JS 接收 Native 的消息 -> JS 执行更新逻辑 -> JS 将更新指令发回 Native
                        ^                                  |
                     Bridge                              Bridge
                        |                                  ↓
[Native thread] Native 发送消息                     Native 触发渲染流程(layout -> mount -> draw)

这个流程最大的问题就是:JS 与 Native 双方无法同步感知对方的执行时机和状态

这也是 useLayoutEffect 在旧架构中无法完全对齐 React Web 语义的根本原因,因为 UI 更新需要通过 Bridge 异步发送到 Native 执行

解决方法也很简单:

  • 在 web 环境下,浏览器作为宿主通过统一的 Event Loop 同时调度 JavaScript 任务与渲染阶段,因此 JS 与 UI 更新天然处于同一套调度体系之下
  • 在 RN 环境下,原生平台就是宿主。如果原生平台能够参与并控制 JavaScript runtime 的任务调度,就可以建立起类似浏览器 Event Loop 的统一调度机制

而这也是 RuntimeScheduler 的核心职责:它允许原生平台参与 JavaScript runtime 的任务调度,从而让 React 的调度系统能够与平台渲染 pipeline 更紧密地协同工作

设计目标

前面我们拿 RuntimeScheduler 类比了浏览器的 Event Loop,但它并非只是简单复刻了 Event loop 的实现,而是为 RN 提供一个宿主级任务调度器,使 JS runtime 的任务调度能够由原生平台参与控制

它有两个主要目标:

  1. 对齐 React 的调度模型:React 的并发渲染和优先级调度最初是围绕浏览器环境设计的,RuntimeScheduler 让 React Scheduler 能够在 React Native 中稳定运行,从而弭平 Web 与原生平台在 React 行为上的差异
  2. 逐步对齐浏览器的宿主环境语义:通过统一 JS runtime 的任务调度机制,RN 可以实现更接近浏览器规范的特性(例如 microtasks、MutationObserver 和 IntersectionObserver 等),这些能力依赖于宿主环境对任务执行顺序和时机的精细控制,而 RuntimeScheduler 正是实现这一能力的关键基础设施

设计图

了解了设计目的后,我们来看看 RuntimeScheduler 是如何设计的:

从上图我们可以看出来,RuntimeScheduler 是一套事件循环调度系统,最终目的都是为了跑 EventLoopTick 的流程(图中右下方的框)

有趣的是,RuntimeScheduler 提供了两种驱动 event loop 的方式:一种是通过任务队列进行异步调度 (对应图中 Async task 的框);另一种是由 Native 同步触发立即执行一次 event loop(对应图中 Native sync event 的框)

下面让我们来详细说明这两个流程:

通过任务队列进行异步调度

这种调度方式是绝大多数状态更新会走的方式,也是最符合 React 设计语义的调用方式

当 React Scheduler 发现某个 root 还有需要异步执行的更新任务时,会按照对应的优先级通过 unstable_scheduleCallback 将 task 交给 RuntimeScheduler;这个 unstable_scheduleCallback 内部方法实际上会调用 RuntimeScheduler 的 scheduleTask 方法(对应图中 React scheduler 指向的框)

scheduleTask 方法会根据当前任务的优先级计算出一个 expirationTime(代表最晚需要什么时候处理这个任务),然后把当前的 task 推入 RuntimeScheduler 内部的优先队列 taskQueue_ 中,taskQueue_ 会根据 expirationTime 排列,确保高优先级任务被优先拿取、低优先级任务不会被遗忘

如果我们看上方的设计图会发现给 taskQueue_ 添加任务的方法总共有三个,他们的区别是:

  • scheduleTask:支持设置优先级,是真正给 taskQueue_ 添加任务的方法
  • scheduleWork:内部会调用 scheduleTask,添加最高优先级的任务
  • scheduleIdleTask:内部会调用 scheduleTask,添加最低优先级的任务

RuntimeScheduler 中的优先级跟 React 是一一对应的:

cpp 复制代码
// in SchedulerPriority.h

enum class SchedulerPriority : int {
  ImmediatePriority = 1,
  UserBlockingPriority = 2,
  NormalPriority = 3,
  LowPriority = 4,
  IdlePriority = 5,
};

每个优先级对应的 expirationTime 如下所示:

cpp 复制代码
// in SchedulerPriorityUtils.h

static inline std::chrono::milliseconds timeoutForSchedulerPriority(
    SchedulerPriority schedulerPriority) noexcept {
  switch (schedulerPriority) {
    case SchedulerPriority::ImmediatePriority:
      // 0 毫秒,马上执行
      return std::chrono::milliseconds(0);
    case SchedulerPriority::UserBlockingPriority:
      // 250 毫秒
      return std::chrono::milliseconds(250);
    case SchedulerPriority::NormalPriority:
      // 5 秒
      return std::chrono::seconds(5);
    case SchedulerPriority::LowPriority:
      // 10 秒
      return std::chrono::seconds(10);
    case SchedulerPriority::IdlePriority:
      // 5 分钟
      return std::chrono::minutes(5);
  }
}

每次调用 scheduleTask 之后且当前没有 Event loop 在运行时,RuntimeScheduler 都会尝试发起 Event loop

一次 Event loop 的过程如下:

  1. 把 Event loop 相关 lambda 推入 RuntimeExecutor,确保是在 JS 线程被执行
  2. 在 lambda 中循环判断 当前 syncTaskRequests_ 是否为 0(syncTaskRequests_ 代表当前需要同步执行的任务,对应到我们前面说到的 Native 同步触发 的情况,后面会详细介绍)
  3. 如果没有同步执行的任务,会抓取当前优先级最高(根据 expirationTime 判断)的任务执行 EventLoopTick
  4. EventLoopTick 执行流程类似浏览器事件循环:宏任务 -> 微任务 -> 渲染窗口(在 RN 中就是 Fabric 的 commit/mount)
  5. 以上步骤 2~4 会循环执行,除非有同步执行任务(syncTaskRequests_ != 0)或者没有可执行任务了(taskQueue_.empty() == true

其中最核心的是 EventLoopTick 的执行流程,它保证了两件事:

  1. 当次 task 所产生的所有微任务都会在渲染之前执行完毕(与浏览器的 event loop 语义保持一致)
  2. 所有在当前 tick 中产生的渲染更新都会被统一收集,并在 updateRendering 阶段一次性执行
代码解析

下面我们来看看真实的代码是怎么写的:

首先是 RuntimeSchedulerBinding,它把 nativeRuntimeScheduler 绑定到了 JS runtime 的 global 上

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerBinding.cpp

std::shared_ptr<RuntimeSchedulerBinding>
RuntimeSchedulerBinding::createAndInstallIfNeeded(
    jsi::Runtime &runtime,
    const std::shared_ptr<RuntimeScheduler> &runtimeScheduler) {
  auto runtimeSchedulerModuleName = "nativeRuntimeScheduler";

  auto runtimeSchedulerValue =
      runtime.global().getProperty(runtime, runtimeSchedulerModuleName);
  if (runtimeSchedulerValue.isUndefined()) {
    auto runtimeSchedulerBinding =
        std::make_shared<RuntimeSchedulerBinding>(runtimeScheduler);
    auto object =
        jsi::Object::createFromHostObject(runtime, runtimeSchedulerBinding);
    // 核心代码:把当前这个 RuntimeSchedulerBinding 类当成 JS 对象绑定到了 global 的 nativeRuntimeScheduler 属性上
    runtime.global().setProperty(runtime, runtimeSchedulerModuleName,
                                 std::move(object));
    return runtimeSchedulerBinding;
  }

  // 省略部分代码
}

RuntimeSchedulerBinding 类中,定义了一堆内部方法(包括我们刚刚说的 unstable_scheduleCallback)这些方法大多指向了真正的 RuntimeScheduler 实现

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerBinding.cpp

jsi::Value RuntimeSchedulerBinding::get(jsi::Runtime &runtime,
                                        const jsi::PropNameID &name) {
  auto propertyName = name.utf8(runtime);

  // 对外暴露的 unstable_scheduleCallback 方法实现
  if (propertyName == "unstable_scheduleCallback") {
    return jsi::Function::createFromHostFunction(
        runtime, name, 3,
        [this](jsi::Runtime &runtime, const jsi::Value &,
               const jsi::Value *arguments, size_t) noexcept -> jsi::Value {
          SchedulerPriority priority = fromRawValue(arguments[0].getNumber());
          auto callback = arguments[1].getObject(runtime).getFunction(runtime);

          // 核心:这里调用了我们说的 scheduleTask 方法
          auto task =
              runtimeScheduler_->scheduleTask(priority, std::move(callback));

          return valueFromTask(runtime, task);
        });
  }
  // 对外暴露的 unstable_scheduleCallback 方法实现,内部调用了 RuntimeScheduler 的 cancelTask 方法
  if (propertyName == "unstable_cancelCallback") {
    // 省略部分代码
  }
  // 对外暴露的 unstable_shouldYield 方法实现,内部调用了 RuntimeScheduler 的 getShouldYield 方法
  if (propertyName == "unstable_shouldYield") {
    // 省略部分代码
  }

  // 省略部分代码
}

RuntimeScheduler 的实现在 0.76.0 版本中有两个:

  • RuntimeScheduler_Legacy:模拟之前 bridge 的 FIFS 队列,兼容老版本
  • RuntimeScheduler_Modern:新架构的实现,也是本文讨论的重点

我们来看看 RuntimeScheduler_Modern 核心方法的具体实现

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp

// scheduleTask 最高优先级的语法糖
void RuntimeScheduler_Modern::scheduleWork(RawCallback &&callback) noexcept {
  SystraceSection s("RuntimeScheduler::scheduleWork");
  // 直接用了最高优先级 SchedulerPriority::ImmediatePriority
  scheduleTask(SchedulerPriority::ImmediatePriority, std::move(callback));
}

// scheduleTask 最低优先级的语法糖
// 下面还有一个 scheduleIdleTask 重载实现,两个唯一的区别在于第一个参数
// jsi::Function 代表这个 task 是从 JS 传过来的
// RawCallback 代表 task 是 Native 侧的
std::shared_ptr<Task> RuntimeScheduler_Modern::scheduleIdleTask(
    jsi::Function &&callback, RuntimeSchedulerTimeout customTimeout) noexcept {
  SystraceSection s("RuntimeScheduler::scheduleIdleTask", "customTimeout",
                    customTimeout.count(), "callbackType", "jsi::Function");

  auto timeout = getResolvedTimeoutForIdleTask(customTimeout);
  auto expirationTime = now_() + timeout;
  // 直接用了最低优先级 SchedulerPriority::IdlePriority
  auto task = std::make_shared<Task>(SchedulerPriority::IdlePriority,
                                     std::move(callback), expirationTime);
  // 内部调用 scheduleTask
  scheduleTask(task);
  return task;
}

std::shared_ptr<Task> RuntimeScheduler_Modern::scheduleIdleTask(
    RawCallback &&callback, RuntimeSchedulerTimeout customTimeout) noexcept {
  // 省略部分代码
}

// scheduleTask 的入口函数,接收优先级以及 task
// 这个方法还有两个参数重载:
// 一个是 RawCallback 类型的 callback 用来接收 Native 侧的任务
// 一个是只接收一个参数的真正实现
std::shared_ptr<Task>
RuntimeScheduler_Modern::scheduleTask(SchedulerPriority priority,
                                      jsi::Function &&callback) noexcept {
  SystraceSection s("RuntimeScheduler::scheduleTask", "priority",
                    serialize(priority), "callbackType", "jsi::Function");

  // 计算优先级的逻辑,timeoutForSchedulerPriority 的实现在上面
  auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
  // 用 Task 封装一下,包含了 expirationTime 信息
  auto task =
      std::make_shared<Task>(priority, std::move(callback), expirationTime);
	// 调用内部的重载函数,这个才是真正的实现
  scheduleTask(task);
  return task;
}

// scheduleTask 的真正实现
void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr<Task> task) {
  bool shouldScheduleEventLoop = false;

  {
    // 加锁,防止 taskQueue_ 被其他线程抢占
    std::unique_lock lock(schedulingMutex_);

    // 如果之前的任务都处理完了且没有正在执行中的任务,就表示准备好执行下一次的 EventLoop 了
    if (taskQueue_.empty() && !isEventLoopScheduled_) {
      isEventLoopScheduled_ = true;
      shouldScheduleEventLoop = true;
    }

    // 添加带有优先级的 task 到优先队列 taskQueue_ 中
    taskQueue_.push(task);
  }

  if (shouldScheduleEventLoop) {
    // 执行 EventLoop
    scheduleEventLoop();
  }
}

// 实现很简单,就是把 runEventLoop 这个 lambda 交给 RuntimeExecutor 执行
void RuntimeScheduler_Modern::scheduleEventLoop() {
  runtimeExecutor_(
      [this](jsi::Runtime &runtime) { runEventLoop(runtime, false); });
}

// 判断当前是否有同步任务,执行 EventLoopTick
void RuntimeScheduler_Modern::runEventLoop(jsi::Runtime &runtime, bool onlyExpired) {
  SystraceSection s("RuntimeScheduler::runEventLoop");
  auto previousPriority = currentPriority_;
  // 如果当前有同步任务,则终止循环
  while (syncTaskRequests_ == 0) {
    auto currentTime = now_();
    auto topPriorityTask = selectTask(currentTime, onlyExpired);
    if (!topPriorityTask) {
      // 没有任务做了,结束循环
      break;
    }
    // 执行 EventLoopTick
    runEventLoopTick(runtime, *topPriorityTask, currentTime);
  }
  currentPriority_ = previousPriority;
}

// 核心代码:执行 宏任务 -> 微任务 -> 渲染窗口
void RuntimeScheduler_Modern::runEventLoopTick(
    jsi::Runtime &runtime, Task &task,
    RuntimeSchedulerTimePoint taskStartTime) {
  SystraceSection s("RuntimeScheduler::runEventLoopTick");

  // 锁住当前的 ShadowTreeRevision
  // 因为在 EventLoopTick 期间 JS 会读取 layout、触发更新
  // 如果此时 Fabric 同时 commit 新树,可能会造成不一致
  ScopedShadowTreeRevisionLock revisionLock(
      shadowTreeRevisionConsistencyManager_);

  // 省略部分代码
  
  // 执行任务(对应浏览器宏任务)
  executeTask(runtime, task, didUserCallbackTimeout);

  if (ReactNativeFeatureFlags::enableMicrotasks()) {
    // 执行微任务
    // 对应规范中的 "Perform a microtask checkpoint"
    performMicrotaskCheckpoint(runtime);
  }

  // 省略部分代码

  if (ReactNativeFeatureFlags::batchRenderingUpdatesInEventLoop()) {
    // 执行 commit、mount 操作
    // 对应规范中的 "Update the rendering"
    updateRendering();
  }
}

Native 同步触发

这种调度方式是通过 RuntimeScheduler_Modern::executeNowOnTheSameThread 方法进行触发的,常用于 Native 需要立即 flush React 更新结果 的场景,这类需求常见于 TextInput、ScrollView 等与用户高频交互的组件

这种调度方式本质上是对 event loop 的一次同步插队执行

可以看设计图的右上方的 Native sync event,当 executeNowOnTheSameThread 被调用的时候,同步执行逻辑如下:

  1. 它会马上创建一个拥有最高优先级的 Task(这个 Task 并不会走上面的 taskQueue_ 那条路,只是为了方便统一 EventLoopTick 的逻辑)
  2. 把 syncTaskRequests_ 的数量 +1,此举会中断 EventLoop 中的循环,让后续异步调度暂缓执行
  3. 【核心逻辑】在当前线程(调用 executeNowOnTheSameThread 方法的线程,不一定是 JS 线程)想办法拿到 JS Runtime 的引用
  4. 在当前线程跑 EventLoopTick
  5. 任务结束,把 syncTaskRequests_ 数量 -1

这个流程保证了特殊任务能够在任务当前线程同步,立刻被执行

代码解析

同步触发的逻辑跟异步调度大同小异,这里只说明两者不同的地方,也就是 executeNowOnTheSameThread 方法:

js 复制代码
// in packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp

// Native 侧可以通过这个方法同步触发任务
void RuntimeScheduler_Modern::executeNowOnTheSameThread(
    RawCallback &&callback) {
  SystraceSection s("RuntimeScheduler::executeNowOnTheSameThread");

  // 先声明一个指针,这个指针在将来会指向 runtime
  // thread_local 关键字代表该变量为当前线程独有变量
  // 意味着如果有别的线程此时也调用了这个方法,它会拿到另一个属于自己的 runtimePtr,而非两个线程共用
  static thread_local jsi::Runtime *runtimePtr = nullptr;

  // 创建一个具有最高优先级的 Task,目的是为了让 EventLoopTick 跑起来
  auto currentTime = now_();
  auto priority = SchedulerPriority::ImmediatePriority;
  auto expirationTime = currentTime + timeoutForSchedulerPriority(priority);
  Task task{priority, std::move(callback), expirationTime};

  // 这个判断非常重要,如果没有它会导致线程死锁
  // 如果同一线程的递归就会导致这个判断为 false 的情况
  // 比如:第一次的 Task 中又调用了 executeNowOnTheSameThread
  // 此时应该走到 else 逻辑中直接开始 EventLoopTick
  // 至于为什么会死锁在后面的 executeSynchronouslyOnSameThread_CAN_DEADLOCK 方法解析会说
  if (runtimePtr == nullptr) {
    syncTaskRequests_++;
    // 核心方法:在这个方法中拿到 runtime 的引用,然后赋值给我们刚刚留的 runtimePtr
    executeSynchronouslyOnSameThread_CAN_DEADLOCK(
        runtimeExecutor_,
        [this, currentTime, &task](jsi::Runtime &runtime) mutable {
          SystraceSection s2(
              "RuntimeScheduler::executeNowOnTheSameThread callback");

          syncTaskRequests_--;
          runtimePtr = &runtime;
    			// 执行 EventLoopTick
          runEventLoopTick(runtime, task, currentTime);
          runtimePtr = nullptr;
        });

  } else {
    // 如果这个方法是递归调用的话,表示我们已经持有 runtime 了
    // 此时就不需要执行 executeSynchronouslyOnSameThread_CAN_DEADLOCK 方法,直接调用即可
    return runEventLoopTick(*runtimePtr, task, currentTime);
  }

	// 下面这段代码完全复制的 scheduleTask 方法,目的是为了执行完同步任务后继续刚刚未执行完的异步任务
  bool shouldScheduleEventLoop = false;
  {
    std::unique_lock lock(schedulingMutex_);
    if (!taskQueue_.empty() && !isEventLoopScheduled_) {
      isEventLoopScheduled_ = true;
      shouldScheduleEventLoop = true;
    }
  }
  if (shouldScheduleEventLoop) {
    scheduleEventLoop();
  }
}

我们知道 executeNowOnTheSameThread 方法的诉求就是:无论任何线程调用它,都能够让 Task 第一时间同步执行完

这里我们出现了第一个卡点:如何取得 runtime 的引用?(无论是执行 Task,还是执行微任务都需要 runtime)

答案是 RuntimeExecutor (在本专栏的上一篇讲 JSI 的文章的 ReactInstance::ReactInstance 章节,我们聊了 RuntimeExecutor 接收一个 callback,然后会在 jsThread 中调用 callback 并传入 runtime)

我们取得 runtime 引用至此分为了两个步骤:

  1. 调用 RuntimeExecutor 并传入一个 callback
  2. 在 callback 中拿到 runtime 的引用然后传回原来的线程

这也是 executeSynchronouslyOnSameThread_CAN_DEADLOCK 的核心逻辑,我们来看看代码:

cpp 复制代码
// in node_modules/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.h

inline static void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
    const RuntimeExecutor &runtimeExecutor,
    std::function<void(jsi::Runtime &runtime)> &&callback) noexcept {
	// 通知 "runtime 已经拿到了"
  std::mutex mutex1;
  // 通知 "外层 callback 已经执行完了"
  std::mutex mutex2;
  // 通知 "内部 lambda 已经彻底结束了"
  std::mutex mutex3;

  // 先把三把锁都锁上
  mutex1.lock();
  mutex2.lock();
  mutex3.lock();

  // 声明一个局部变量 runtimePtr
  jsi::Runtime *runtimePtr;

  // 取得当前线程的 id
  auto threadId = std::this_thread::get_id();

  // 调用 runtimeExecutor,传入 lambda
  runtimeExecutor([&](jsi::Runtime &runtime) {
    // 拿到 runtime 引用了!
    // 赋值给局部变量 runtimePtr
    runtimePtr = &runtime;

    // 如果当前线程就是 runtimeExecutor 指定的线程(通常就是 JS 线程)
    if (threadId == std::this_thread::get_id()) {
      // 不存在跨线程调用了,直接把 mutex1 跟 mutex3 解开
      // mutex1.unlock 后外层(runtimeExecutor 后面)的 callback(*runtimePtr) 就会马上被执行
      //(callback 就是 executeNowOnTheSameThread 方法中 executeSynchronouslyOnSameThread_CAN_DEADLOCK 的 lambda) 
      mutex1.unlock();
      // mutex3 解开后表示当前 lambda 执行完成了,通知外层可以结束了
      mutex3.unlock();
      return;
    }

    mutex1.unlock();
    // 这里 lock 住是为了等到外层 callback(*runtimePtr); 执行完
    // 在这个过程中,runtimeExecutor 指定的线程会被阻塞
    // 防止 runtime 同时被多个线程使用
    mutex2.lock();
    mutex3.unlock();
  });

  mutex1.lock();
  callback(*runtimePtr);
  mutex2.unlock();
  mutex3.lock();
}

为了方便理解,我们来看看执行过程线程的情况:

js 复制代码
// 同一个线程(caller thread == runtimeExecutor thread)
────────────────────────────────────────────
executeNowOnTheSameThread
        │
        ▼
executeSynchronouslyOnSameThread_CAN_DEADLOCK
        │
        ▼
runtimeExecutor(lambda)
        │
        ▼
lambda(runtime) 立即执行
        │
        ▼
callback(runtimePtr)
        │
        ▼
runEventLoopTick(runtime, task)
        │
        ▼
       返回
js 复制代码
// 不同线程(caller thread !== runtimeExecutor thread)
Caller Thread                         runtimeExecutor Thread(JS thread)
──────────────────────────────        ──────────────────────────────

executeNowOnTheSameThread
        │
        ▼
executeSynchronouslyOnSameThread_CAN_DEADLOCK
        │
        ▼
runtimeExecutor(lambda)   ───────────▶   lambda(runtime)
                                         │
                                         ▼
   (等待中...)                     runtimePtr = &runtime
                                         │
                                         ▼
                                    runtime 引用准备完成
        │
        │
        ▼
callback(runtimePtr)
        │
        ▼
runEventLoopTick(runtime, task)
        │
        ▼
       返回

总结

本文聊了 RuntimeScheduler 的核心机制:它在 React Native 的原生环境中实现了一套接近浏览器语义的 Event Loop,通过统一调度 task、microtask 以及渲染更新的执行顺序,使 React 的并发调度模型能够在 Native 平台上正常运行

RuntimeScheduler 解决的只是 "什么时候执行更新" 的问题,真正负责把 React 的更新转换为 UI 的,其实是 React Native 新架构中的 Fabric 渲染器

因此,在理解了 RuntimeScheduler 之后,在本专栏后续章节,我们就可以顺着 updateRendering 继续往下看:Fabric 是如何接管这一步,并完成从 React 更新到最终原生 UI 渲染的全过程的

相关推荐
sensen_kiss2 小时前
CAN302 电子商务技术 Pt.1 Web技术导论
前端·网络·学习
ProgramHan2 小时前
十大排行榜——前端语言及要介绍
前端
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-permissions
javascript·react native·react.js
氢灵子2 小时前
Fixed 定位的失效问题
前端·javascript·css
haibindev3 小时前
把近5万个源文件喂给AI之前,我先做了一件事
java·前端·c++·ai编程·代码审计·架构分析
labixiong3 小时前
React Hooks 闭包陷阱:高级场景与深度思考
前端·javascript·react.js
早點睡3903 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-contacts
javascript·react native·react.js
☞无能盖世♛逞何英雄☜3 小时前
Echarts数据可视化应用
前端·信息可视化·echarts
2501_943610363 小时前
我爱导航系统美化版源码网址导航系统带后台-【全开源】
前端·后端·html·php