从零实现 React v18,但 WASM 版 - [27] 实现 useTransition使用 Rust 和 Web
模仿 big-react,使用 Rust 和 WebAssembly,从零实现 React v18 的核心功能。深入理解 React 源码的同时,还锻炼了 Rust 的技能,简直赢麻了!
本文对应 tag:v27
useTransition
是 React 中一个新推出的 Hook,可以让你在不阻塞 UI 的情况下更新状态。官网这里有个例子对比了使用它前后的区别,这里也有篇文章分析了一下原理。接下来我们就来实现一下,本次改动详情见这里。
我们先按照之前新增 Hook 的流程把相关代码都加上,最后会来到 fiber_hooks.rs
:
fn mount_transition() -> Vec<JsValue> {
let result = mount_state(&JsValue::from(false)).unwrap();
let is_pending = result[0].as_bool().unwrap();
let set_pending = result[1].clone().dyn_into::<Function>().unwrap();
let hook = mount_work_in_progress_hook();
let set_pending_cloned = set_pending.clone();
let closure = Closure::wrap(Box::new(move |callback: Function| {
start_transition(set_pending_cloned.clone(), callback);
}) as Box<dyn Fn(Function)>);
let start: Function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
hook.as_ref().unwrap().clone().borrow_mut().memoized_state =
Some(MemoizedState::MemoizedJsValue(start.clone().into()));
vec![JsValue::from_bool(is_pending), start.into()]
}
mount_transition
时,会形成如下的数据结构:
所以 update_transition
时,依次取出 Hooks 上面的值即可:
fn update_transition() -> Vec<JsValue> {
let result = update_state(&JsValue::undefined()).unwrap();
let is_pending = result[0].as_bool().unwrap();
let hook = update_work_in_progress_hook();
if let MemoizedState::MemoizedJsValue(start) = hook
.as_ref()
.unwrap()
.clone()
.borrow()
.memoized_state
.as_ref()
.unwrap()
{
return vec![JsValue::from_bool(is_pending), start.into()];
}
panic!("update_transition")
}
关键在于 startTransition
的实现:
fn start_transition(set_pending: Function, callback: Function) {
set_pending.call1(&JsValue::null(), &JsValue::from_bool(true));
let prev_transition = unsafe { REACT_CURRENT_BATCH_CONFIG.transition };
// low priority
unsafe { REACT_CURRENT_BATCH_CONFIG.transition = Lane::TransitionLane.bits() };
callback.call0(&JsValue::null());
set_pending.call1(&JsValue::null(), &JsValue::from_bool(false));
unsafe { REACT_CURRENT_BATCH_CONFIG.transition = prev_transition };
}
根据这篇文章的分析结果,这里首先以当前优先级将 isPending
更新为 true
。然后降低优先级,执行 callbak
并将 isPending
更新为 false
。最后,将优先级恢复为原来的优先级。
降低优先级后的更新流程会使用 Concurrent Mode,这也是不阻塞 UI 的原因:
if cur_priority == Lane::SyncLane {
...
} else {
if is_dev() {
log!("Schedule in macrotask, priority {:?}", update_lanes);
}
let scheduler_priority = lanes_to_scheduler_priority(cur_priority.clone());
let closure = Closure::wrap(Box::new(move |did_timeout_js_value: JsValue| {
let did_timeout = did_timeout_js_value.as_bool().unwrap();
perform_concurrent_work_on_root(root_cloned.clone(), did_timeout)
}) as Box<dyn Fn(JsValue) -> JsValue>);
let function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
new_callback_node = Some(unstable_schedule_callback_no_delay(
scheduler_priority,
function,
))
}
到这,useTransition
的实现基本上就完成了,不过实现过程中遇到了几个 bug:
第一个 Bug 在 begin_work.rs
中:
work_in_progress.borrow_mut().lanes = Lane::NoLane;
当一个 FiberNode 上有多个 Lane 时,这样会有问题,应该改成:
work_in_progress.borrow_mut().lanes -= render_lane;
即每次只把当前渲染的 Lane 给去掉。
第二个 Bug 在 work_loop.rs
中:
log!("render over {:?}", *root.clone().borrow());
WORK_IN_PROGRESS_ROOT_RENDER_LANE = Lane::NoLane;
原来这一句是在 render_root
函数中,即 Render 阶段结束后,将该变量重置。但是 Concurrent Mode 模式下,当 Render 流程被打断后,不应该重置这个变量。所以,把这一句移到了 perform_concurrent_work_on_root
:
if exit_status == ROOT_COMPLETED {
...
unsafe { WORK_IN_PROGRESS_ROOT_RENDER_LANE = Lane::NoLane };
}
即只有当 Render 流程完整的结束了以后才重置这个变量。
第三个 Bug 在 update_queue.rs
中,如下所示:
另外,还重构了 Scheduler。之前的小顶堆是这样定义的:
static mut TASK_QUEUE: Vec<Task> = vec![];
static mut TIMER_QUEUE: Vec<Task> = vec![];
这样就导致当我们需要修改堆中 Task
中的属性时,还得单独实现一个 peek_mut
的函数:
let mut task = peek_mut(&mut TASK_QUEUE);
task.callback = JsValue::null();
现在改成这样:
static mut TASK_QUEUE: Vec<Rc<RefCell<Task>>> = vec![];
static mut TIMER_QUEUE: Vec<Rc<RefCell<Task>>> = vec![];
统一使用 peek
就可以了:
let task = peek(&TASK_QUEUE);
task.borrow_mut().callback = JsValue::null();
跪求 star 并关注公众号”前端游“。
转载自:https://juejin.cn/post/7418505553642143795