从零实现 React v18,但 WASM 版 - [3] Renderer 和 Reconciler 架构设计

模仿 big-react,使用 Rust 和 WebAssembly,从零实现 React v18 的核心功能。深入理解 React 源码的同时,还锻炼了 Rust 的技能,简直赢麻了!

代码地址:github.com/ParadeTo/bi...

本文对应 tag:v3

前言

上一篇文章末本计划本篇实现 render 阶段,但考虑到内容太多,还是分开写比较好。说是架构设计,有点夸张了,其实主要的目的在于要实现 RendererReconciler 的解耦,让 Reconciler 可以支持不同的 Renderer,因为后续还需要实现 ReactNoop Renderer 用来跑测试用例。

Reconciler

Rust 中要实现我们的目的,离不开 Trait,所以我们在 Reconciler 中先定义好 HostConfig Trait:

rust 复制代码
// react-reconciler/src/lib.rs
pub trait HostConfig {
    fn create_text_instance(&self, content: String) -> Rc<dyn Any>;
    fn create_instance(&self, _type: Rc<dyn Any>) -> Rc<dyn Any>;
    fn append_initial_child(&self, parent: Rc<dyn Any>, child: Rc<dyn Any>);
    fn append_child_to_container(&self, child: Rc<dyn Any>, parent: Rc<dyn Any>);
}

这里暂时只定义了部分方法,需要注意的是由于不同 Renderer 下的 stateNode 是不一样的,这里的类型都不能确定,所以使用了 std::any::Any

接着来定义 Reconciler

rust 复制代码
// react-reconciler/src/lib.rs
pub struct Reconciler {
    host_config: Box<dyn HostConfig>,
}

impl Reconciler {
    pub fn new(host_config: Box<dyn HostConfig>) -> Self {
        Reconciler { host_config }
    }
    pub fn create_container(&self, container: &JsValue) -> Rc<RefCell<FiberRootNode>> {
        Rc::new(RefCell::new(FiberRootNode {}))
    }

    pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
        log!("{:?} {:?}", element, root)
    }
}

Reconciler 中包含 host_config 属性,使用 Trait Object 来表示泛型,当某个 Renderer 中实例化一个 Reconciler 对象时,需要传入实现了 HostConfig Trait 的类型的实例。

其他两个方法先简单的实现一下,供调试用。接下来看看 Renderer

Renderer

Renderer 中当然首先要实现 HostConfig

rust 复制代码
// react-dom/src/host_config.rs
use react_reconciler::HostConfig;


impl HostConfig for ReactDomHostConfig {
    fn create_text_instance(&self, content: String) -> Rc<dyn Any> {
        let window = window().expect("no global `window` exists");
        let document = window.document().expect("should have a document on window");
        Rc::new(document.create_text_node(content.as_str()))
    }

    fn create_instance(&self, _type: String) -> Rc<dyn Any> {
        let window = window().expect("no global `window` exists");
        let document = window.document().expect("should have a document on window");
        match document.create_element(_type.as_ref()) {
            Ok(element) => Rc::new(element),
            Err(_) => todo!(),
        }
    }

    fn append_initial_child(&self, parent: Rc<dyn Any>, child: Rc<dyn Any>) {
        let parent = parent.clone().downcast::<Element>().unwrap();
        let child = child.downcast::<Text>().unwrap();
        match parent.append_child(&child) {
            Ok(_) => {
                log!("append_initial_child successfully ele {:?} {:?}", parent, child);
            }
            Err(_) => todo!(),
        }
    }

    fn append_child_to_container(&self, child: Rc<dyn Any>, parent: Rc<dyn Any>) {
        todo!()
    }
}

可以看到我们可以通过 downcastAny 类型转化为具体的类型,这里暂时都很简单地实现了一下。

然后我们定义一个 Renderer

rust 复制代码
// react-dom/src/renderer.rs
#[wasm_bindgen]
pub struct Renderer {
    root: Rc<RefCell<FiberRootNode>>,
    reconciler: Reconciler,
}

impl Renderer {
    pub fn new(root: Rc<RefCell<FiberRootNode>>, reconciler: Reconciler) -> Self {
        Self { root, reconciler }
    }
}

#[wasm_bindgen]
impl Renderer {
    pub fn render(&self, element: &JsValue) {
        self.reconciler.update_container(Rc::new(element.clone()), self.root.clone())
    }
}

他包含 rootreconciler 两个属性,其中 root 是在调用 create_root 时通过 reconcilercreate_container 方法生成的:

rust 复制代码
#[wasm_bindgen(js_name = createRoot)]
pub fn create_root(container: &JsValue) -> Renderer {
    set_panic_hook();
    let reconciler = Reconciler::new(Box::new(ReactDomHostConfig));
    let root = reconciler.create_container(container);
    let renderer = Renderer::new(root, reconciler);
    renderer
}

测试

一切就绪了,我们加点代码来调试一下,我们把 hello-world 中的例子改成如下所示:

js 复制代码
import {createRoot} from 'react-dom'

const comp = <div>hello world</div>
const root = createRoot(document.getElementById('root'))
root.render(comp)

然后在 Reconciler 中,我们先硬编码实现首次渲染:

rust 复制代码
pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
    let props = Reflect::get(&*element, &JsValue::from_str("props")).unwrap();
    let _type = Reflect::get(&*element, &JsValue::from_str("type")).unwrap();
    let children = Reflect::get(&props, &JsValue::from_str("children")).unwrap();

    let text_instance = self.host_config.create_text_instance(children.as_string().unwrap());
    let div_instance = self.host_config.create_instance(_type.as_string().unwrap());
    self.host_config.append_initial_child(div_instance.clone(), text_instance);

    let window = window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().expect("document should have a body");

    body.append_child(&*div_instance.clone().downcast::<Element>().unwrap());
}

不出意外,重新构建并安装依赖,运行 hello world 项目就可以在浏览器中看到内容了。

跪求 star 并关注公众号"前端游"。

相关推荐
不是鱼3 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
飞翔的渴望5 小时前
antd3升级antd5总结
前端·react.js·ant design
╰つ゛木槿9 小时前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
itas10910 小时前
Rust调用C动态库
c语言·rust·bindgen·bindings·rust c绑定
SomeB1oody10 小时前
【Rust自学】5.1. 定义并实例化struct
开发语言·后端·rust
用户305875848912512 小时前
Connected-react-router核心思路实现
react.js
m0_7482361113 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
天涯学馆1 天前
解锁WebAssembly与JavaScript交互的无限可能
前端·webassembly
哑巴语天雨1 天前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情1 天前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js