从零实现 React v18,但 WASM 版 - [10] 实现单节点更新流程
模仿 big-react,使用 Rust 和 WebAssembly,从零实现 React v18 的核心功能。深入理解 React 源码的同时,还锻炼了 Rust 的技能,简直赢麻了!
本文对应 tag:v10
上上篇文章末尾说了我们目前还没有完整的实现更新流程,所以这篇文章我们来实现一下。
还是用之前的例子:
function App() {
const [name, setName] = useState(() => 'ayou')
setTimeout(() => {
setName('ayouayou')
}, 1000)
return (
<div>
<Comp>{name}</Comp>
</div>
)
}
当我们调用 setName('ayouayou')
时,会触发更新流程,而 setName
这个方法是在首次渲染的时候在 mount_state
中返回的,该方法会在当前 FiberNode
的 memoized_state
上挂载一个 Hook
节点,如果有多个 Hooks, 会形成一个链表。Hook
节点上有个 update_queue
,显而易见,这是个更新队列。还有个 memoized_state
属性,记录当前 Hook
的状态:
fn mount_state(initial_state: &JsValue) -> Result<Vec<JsValue>, JsValue> {
// Add hook to current FiberNode memoized_state
let hook = mount_work_in_progress_hook();
let memoized_state: JsValue;
if initial_state.is_function() {
memoized_state = initial_state
.dyn_ref::<Function>()
.unwrap()
.call0(&JsValue::null())?;
} else {
memoized_state = initial_state.clone();
}
hook.as_ref().unwrap().clone().borrow_mut().memoized_state =
Some(MemoizedState::JsValue(memoized_state.clone()));
let queue = create_update_queue();
hook.as_ref().unwrap().clone().borrow_mut().update_queue = Some(queue.clone());
...
}
mount_state
最终会返回 initial_state
和一个函数:
let q_rc = Rc::new(queue.clone());
let q_rc_cloned = q_rc.clone();
let fiber = unsafe {
CURRENTLY_RENDERING_FIBER.clone().unwrap()
};
let closure = Closure::wrap(Box::new(move |action: &JsValue| unsafe {
dispatch_set_state(
fiber.clone(),
(*q_rc_cloned).clone(),
action,
)
}) as Box<dyn Fn(&JsValue)>);
let function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
queue.clone().borrow_mut().dispatch = Some(function.clone());
Ok(vec![memoized_state, function.into()])
这里有点奇怪的是 closure
中的 q_rc_cloned
,明明 queue
已经是个 Rc
类型了,为什么还要在外面再包一层 Rc
?因为如果把 (*q_rc_cloned).clone()
改成 queue.clone()
,会报如下错误:
error[E0382]: borrow of moved value: `queue`
--> packages/react-reconciler/src/fiber_hooks.rs:251:5
|
233 | let queue = create_update_queue();
| ----- move occurs because `queue` has type `Rc<RefCell<UpdateQueue>>`, which does not implement the `Copy` trait
...
240 | let closure = Closure::wrap(Box::new(move |action: &JsValue| unsafe {
| ----------------------- value moved into closure here
...
243 | queue.clone(),
| ----- variable moved due to use in closure
...
251 | queue.clone().borrow_mut().dispatch = Some(function.clone());
| ^^^^^ value borrowed here after move
原因在于 queue
的值的所有权已经被 move 进闭包中了,外面不能再继续使用了。那去掉 move 行么?试试看,结果发现会报这个错误:
error[E0597]: `queue` does not live long enough
--> packages/react-reconciler/src/fiber_hooks.rs:243:13
|
240 | let closure = Closure::wrap(Box::new(|action: &JsValue| unsafe {
| - ------------------ value captured here
| _________________________________|
| |
241 | | dispatch_set_state(
242 | | fiber.clone(),
243 | | queue.clone(),
| | ^^^^^ borrowed value does not live long enough
... |
246 | | )
247 | | }) as Box<dyn Fn(&JsValue)>);
| |______- cast requires that `queue` is borrowed for `'static`
...
254 | }
| - `queue` dropped here while still borrowed
原因在于,如果不 move 进去,queue
在 mount_state
执行完后就会被回收,而闭包里面却仍然在借用,显然不行。
都说 Rust 学习曲线陡峭的原因就在此,大部分时候都在和编译器作斗争。不过 Rust 的理念就是这样,在程序编译时就把大部分的问题给发现出来,这样修复的效率比上线后发现再修复的效率要高得多。而且,Rust 编译器也很智能,给出的问题描述一般都很清晰。
继续回到使用 move 和 queue
的错误。分析一下,因为 queue
被 move 了,所以后面不能使用 queue
,那么如果我们 move 一个别的值不就可以了么,所以就有了 queue_rc
,两者的内存模型对比如下所示:
还有一个值得说明的地方是,我们把这个闭包函数挂载到了每个 Hook
节点的 dispatch
属性上:
queue.clone().borrow_mut().dispatch = Some(function.clone());
是为了在 update_state
时返回同样的函数:
fn update_state(initial_state: &JsValue) -> Result<Vec<JsValue>, JsValue> {
...
Ok(vec![
hook.clone().unwrap().clone()
.borrow()
.memoized_state
.clone()
.unwrap()
.js_value()
.unwrap().clone(),
queue.clone().unwrap().borrow().dispatch.clone().into(),
])
}
不过我感觉这个 dispatch
作为 Hook
的属性更合适,至少目前来看它跟 queue
好像没什么关联。
回到代码,当调用 dispatch
时,最后会调用 dispatch_set_state
:
fn dispatch_set_state(
fiber: Rc<RefCell<FiberNode>>,
update_queue: Rc<RefCell<UpdateQueue>>,
action: &JsValue,
) {
let update = create_update(action.clone());
enqueue_update(update_queue.clone(), update);
unsafe {
WORK_LOOP
.as_ref()
.unwrap()
.clone()
.borrow()
.schedule_update_on_fiber(fiber.clone());
}
}
它的作用就是使用传入的 action
更新 Hook
节点的 update_queue
,并开启一轮新的更新流程,此时 App
节点状态如下图所示:
接下来流程跟首次渲染类似,首先看 begin work,更新过程的 begin work 主要是对于 FiberNode
的子节点的处理,它通过当前 Fiber Tree 中的子 FiberNode
节点和新产生的 ReactElement
(代码中叫做 children)来生成新的子 FiberNode
,也就是我们常说的 diff 过程:
其中,不同类型的 FiberNode
节点产生 children 的方式有所不同:
HostRoot
:从memoized_state
取值HostComponent
:从pending_props
中取值FunctionComponent
:通过执行type
指向的Function
来得到HostText
:没有这个过程,略
而如何产生这个新的子 FiberNode
,也有两种情况:
- Diff 的
ReactElement
和FiberNode
的key
和type
都一样。复用FiberNode
,使用ReactElement
上的props
来更新FiberNode
中的pending_props
:
- 其他情况。创建新的
FiberNode
,并在父节点打上ChildDeletion
标记,同时把旧的FiberNode
添加到deletions
列表中:
代码就不贴了,可以查看本次改动的 child_fiber
文件。
由于 FunctionComponent
产生 children 的方式相对复杂一点,我们再回过头来看看 render_with_hooks
方法,主要改动点为:
pub fn render_with_hooks(work_in_progress: Rc<RefCell<FiberNode>>) -> Result<JsValue, JsValue> {
...
if current.is_some() {
// log!("还未实现update时renderWithHooks");
update_hooks_to_dispatcher(true);
} else {
update_hooks_to_dispatcher(false);
}
...
}
fn update_hooks_to_dispatcher(is_update: bool) {
let object = Object::new();
let closure = Closure::wrap(Box::new(if is_update { update_state } else { mount_state })
as Box<dyn Fn(&JsValue) -> Result<Vec<JsValue>, JsValue>>);
let function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
Reflect::set(&object, &"use_state".into(), &function).expect("TODO: panic set use_state");
updateDispatcher(&object.into());
}
也就是在更新的时候把 dispatcher
里的 use_state
更新为 update_state
方法,而 update_state
中主要是根据 Hooks
上的 update_queue
和 memoized_state
计算出新的 memoized_state
进行返回,同时还返回了 dispatch
函数:
fn update_state(initial_state: &JsValue) -> Result<Vec<JsValue>, JsValue> {
let hook = update_work_in_progress_hook();
let hook_cloned = hook.clone().unwrap().clone();
let queue = hook_cloned.borrow().update_queue.clone();
let base_state = hook_cloned.borrow().memoized_state.clone();
unsafe {
hook_cloned.borrow_mut().memoized_state = process_update_queue(
base_state,
queue.clone(),
CURRENTLY_RENDERING_FIBER.clone().unwrap(),
);
}
Ok(vec![
hook.clone().unwrap().clone()
.borrow()
.memoized_state
.clone()
.unwrap()
.js_value()
.unwrap().clone(),
queue.clone().unwrap().borrow().dispatch.clone().into(),
])
}
begin work 阶段就说这么多,接下来看看 complete work 阶段,complete work 阶段相对来说简单一点,主要是对节点进行 Update
标记,修改了处理 HostText
和 HostComponent
的逻辑:
WorkTag::HostText => {
if current.is_some() && work_in_progress_cloned.borrow().state_node.is_some() {
// reuse FiberNode
let old_text = derive_from_js_value(¤t.clone().unwrap().clone().borrow().memoized_props, "content");
let new_test = derive_from_js_value(&new_props, "content");
if !Object::is(&old_text, &new_test) {
CompleteWork::mark_update(work_in_progress.clone());
}
} else {
let text_instance = self.host_config.create_text_instance(
Reflect::get(&new_props, &JsValue::from_str("content"))
.unwrap()
.as_string()
.unwrap(),
);
work_in_progress.clone().borrow_mut().state_node =
Some(Rc::new(StateNode::Element(text_instance.clone())));
}
self.bubble_properties(work_in_progress.clone());
None
},
WorkTag::HostComponent => {
if current.is_some() && work_in_progress_cloned.borrow().state_node.is_some() {
// reuse FiberNode
log!("TODO: update properties")
} else {
let instance = self.host_config.create_instance(
work_in_progress
.clone()
.borrow()
._type
.as_ref()
.as_string()
.unwrap(),
);
self.append_all_children(instance.clone(), work_in_progress.clone());
work_in_progress.clone().borrow_mut().state_node =
Some(Rc::new(StateNode::Element(instance.clone())));
}
self.bubble_properties(work_in_progress.clone());
None
}
最后是 commit 阶段,主要就是在 commit_mutation_effects_on_fiber
中增加对 Update
和 ChildDeletion
的处理:
fn commit_mutation_effects_on_fiber(&self, finished_work: Rc<RefCell<FiberNode>>) {
...
if flags.contains(Flags::ChildDeletion) {
let deletions = finished_work.clone().borrow().deletions.clone();
if deletions.is_some() {
let deletions = deletions.unwrap();
for child_to_delete in deletions {
self.commit_deletion(child_to_delete);
}
}
finished_work.clone().borrow_mut().flags -= Flags::ChildDeletion;
}
if flags.contains(Flags::Update) {
self.commit_update(finished_work.clone());
finished_work.clone().borrow_mut().flags -= Flags::Update;
}
...
Update
中目前只处理了 HostText
,比较简单,就不介绍了,直接看代码吧,这里重点介绍下 ChildDeletion
。
begin work 中我们说过标记为删除的子节点会被加入父节点的 deletions
列表中,所以这里会遍历这个列表,然后调用 commit_deletion
,该函数会采取前序的方式遍历(优先遍历根节点) child_to_delete
为根节点的子树,并执行这些节点上相关的副作用,如:执行 componentWillUnmount
方法或 useEffect
返回的 destroy
方法,从这里也可以发现父组件的副作用会先执行。
比如下面这个例子:
的遍历顺序为 div->p->i->span
。同时还会记录第一个遍历到的节点,此例为 div
,然后在该节点上执行删除操作。
好了,单节点更新流程就实现完毕了,简单总结下就是:
- 在 begin work 阶段中标记子节点的删除、插入
- complete work 阶段中标记节点的更新
- commit 流程中深度优先遍历 Fiber Tree,处理有标记的节点。对于标记为
ChildDeletion
的节点,会采用前序遍历的方式遍历以此节点为根节点的子树。
更多详见本次更新。
跪求 star 并关注公众号“前端游”
转载自:https://juejin.cn/post/7362788635122745359