模仿 big-react,使用 Rust 和 WebAssembly,从零实现 React v18 的核心功能。深入理解 React 源码的同时,还锻炼了 Rust 的技能,简直赢麻了!
代码地址:https://github.com/ParadeTo/big-react-wasm
本文对应 tag:v24
Based on big-react,I am going to implement React v18 core features from scratch using WASM and Rust.
Code Repository:https://github.com/ParadeTo/big-react-wasm
The tag related to this article:v24
Suspense 无疑是新版 react 中最吸引人的一个特性,所以我们也来实现一下。本文是第一部分,实现 Suspense 的 Fallback 渲染。
以下面代码为例:
1 | import {Suspense} from 'react' |
对于 Suspense
节点来说,他有两条子分支,分别对应 Primary
和 Fallback
,其中 Primary
分支的根节点为 Offscreen
类型的节点,Fallback
分支的根节点为 Fragment
类型的节点:
具体到上面的例子则为:
首次渲染时,会进入 Primary 分支,当处理到 Child
组件时,由于该组件抛出了 Promise
对象,开始进入 unwind
流程,该流程会往上找到最近的 Suspense
节点,并添加 DidCapture
的标记,接着从该节点继续 render 流程。
这次因为 Suspense
节点上有 DidCapture
标记,所以会进入 Fallback 分支,接下来就是正常的 render 和 commit 流程,最终渲染出 Fallback 中的内容。
这就是本次我们要实现的功能,下面来简单过一下代码。
首先,还是看看 begin_work.rs
,需要新增对于 Suspense
的处理:
1 | fn update_suspense_component( |
这里,根据当前是否显示 Fallback 以及是否为首次更新分为了四个分支来处理。
接下来,看看 work_loop.rs
:
1 | loop { |
当组件中抛出异常时,会进入 Err 的分支,这里主要是增加了 handle_throw
流程,目前比较简单:
1 | fn handle_throw(root: Rc<RefCell<FiberRootNode>>, thrown_value: JsValue) { |
接着,循环继续,进入 unwind
流程:
1 | loop { |
1 | fn throw_and_unwind_work_loop( |
这里的任务就是往上找到最近的 Suspense
节点,并标记 DidCapture
。
到这我们的任务就完成了,不过为了给下一篇文章多铺点路,我们再来多实现一点功能。
还是以上面代码为例,首次渲染处理到 Child
组件时,应该要捕获到其抛出的 Promise
对象,并调用它的 then
方法,然后在传入的函数中触发重新渲染的逻辑。
这样,当 Promise
对象状态变成 fullfilled 后,会再次进入 render 流程,此时处理到 Child
组件仍然会抛出异常,结果就是不停重复上面的流程,不过没关系,我们暂时不处理,因为目前我们还没有实现 use
hook,暂时只能这样来测试。
我们来看看怎么捕获 Promise
对象并在对象 fullfilled
时,重新开启渲染流程:
首先,我们在 throw_and_unwind_work_loop
中添加 throw_exception
方法:
1 | fn throw_and_unwind_work_loop( |
其中 ping
函数就是传入 then
的函数,核心逻辑就是把当前的 lane
作为下次更新的优先级,并调用 ensure_root_is_scheduled
开启新的更新。不过测试发现,这样还不够,因为 begin_work.rs
中性能优化的功能会从根节点开始 bailout 掉这次更新,big-react 也有这个问题(切换到 master 分支并运行 suspense-use 这个例子可以复现,详见 issue)。
解决这个问题,权宜之计是在 unwind 流程之前,把更新优先级再网上冒泡一次,这样当再次从根节点开始更新时,由于 subtree_flags
上的标记,就不会进入 bailout 的流程了。
1 | loop { |
本次更新详见这里。