本文所有代码如果没有特别标注的话,默认用的都是 v0.76.0 的 RN 代码
为什么需要 RuntimeScheduler
在新架构的综述文章中我们聊过旧版本 RN 典型的渲染流程是:
JS 接收 Native 的消息
↓
JS 执行更新逻辑
↓
JS 通过 Bridge 将更新指令发回 Native
↓
Native 触发渲染流程
现在我们尝试用线程的视角重新看一下这个流程:
[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 的任务调度能够由原生平台参与控制
它有两个主要目标:
- 对齐 React 的调度模型:React 的并发渲染和优先级调度最初是围绕浏览器环境设计的,RuntimeScheduler 让 React Scheduler 能够在 React Native 中稳定运行,从而弭平 Web 与原生平台在 React 行为上的差异
- 逐步对齐浏览器的宿主环境语义:通过统一 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 是一一对应的:
// in SchedulerPriority.h
enum class SchedulerPriority : int {
ImmediatePriority = 1,
UserBlockingPriority = 2,
NormalPriority = 3,
LowPriority = 4,
IdlePriority = 5,
};
每个优先级对应的 expirationTime 如下所示:
// 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 的过程如下:
- 把 Event loop 相关 lambda 推入 RuntimeExecutor,确保是在 JS 线程被执行
- 在 lambda 中循环判断 当前 syncTaskRequests_ 是否为 0(syncTaskRequests_ 代表当前需要同步执行的任务,对应到我们前面说到的 Native 同步触发 的情况,后面会详细介绍)
- 如果没有同步执行的任务,会抓取当前优先级最高(根据 expirationTime 判断)的任务执行 EventLoopTick
- EventLoopTick 执行流程类似浏览器事件循环:宏任务 -> 微任务 -> 渲染窗口(在 RN 中就是 Fabric 的 commit/mount)
- 以上步骤 2~4 会循环执行,除非有同步执行任务(
syncTaskRequests_ != 0)或者没有可执行任务了(taskQueue_.empty() == true)
其中最核心的是 EventLoopTick 的执行流程,它保证了两件事:
- 当次 task 所产生的所有微任务都会在渲染之前执行完毕(与浏览器的 event loop 语义保持一致)
- 所有在当前 tick 中产生的渲染更新都会被统一收集,并在 updateRendering 阶段一次性执行
代码解析
下面我们来看看真实的代码是怎么写的:
首先是 RuntimeSchedulerBinding,它把 nativeRuntimeScheduler 绑定到了 JS runtime 的 global 上
// 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 实现
// 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 核心方法的具体实现
// 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 被调用的时候,同步执行逻辑如下:
- 它会马上创建一个拥有最高优先级的 Task(这个 Task 并不会走上面的
taskQueue_那条路,只是为了方便统一 EventLoopTick 的逻辑) - 把 syncTaskRequests_ 的数量 +1,此举会中断 EventLoop 中的循环,让后续异步调度暂缓执行
- 【核心逻辑】在当前线程(调用
executeNowOnTheSameThread方法的线程,不一定是 JS 线程)想办法拿到 JS Runtime 的引用 - 在当前线程跑 EventLoopTick
- 任务结束,把 syncTaskRequests_ 数量 -1
这个流程保证了特殊任务能够在任务当前线程同步,立刻被执行
代码解析
同步触发的逻辑跟异步调度大同小异,这里只说明两者不同的地方,也就是 executeNowOnTheSameThread 方法:
// 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 引用至此分为了两个步骤:
- 调用 RuntimeExecutor 并传入一个 callback
- 在 callback 中拿到 runtime 的引用然后传回原来的线程
这也是 executeSynchronouslyOnSameThread_CAN_DEADLOCK 的核心逻辑,我们来看看代码:
// 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();
}
为了方便理解,我们来看看执行过程线程的情况:
// 同一个线程(caller thread == runtimeExecutor thread)
────────────────────────────────────────────
executeNowOnTheSameThread
│
▼
executeSynchronouslyOnSameThread_CAN_DEADLOCK
│
▼
runtimeExecutor(lambda)
│
▼
lambda(runtime) 立即执行
│
▼
callback(runtimePtr)
│
▼
runEventLoopTick(runtime, task)
│
▼
返回
// 不同线程(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 渲染的全过程的