从零实现 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 并关注公众号"前端游"。

相关推荐
1234Wu1 小时前
React Native 接入 eCharts
javascript·react native·react.js
寻月隐君1 小时前
Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx)
后端·rust·github
芳草萋萋鹦鹉洲哦9 小时前
【vue3+tauri+rust】如何实现下载文件mac+windows
windows·macos·rust
小白变怪兽12 小时前
一、react18+项目初始化(vite)
前端·react.js
寻月隐君17 小时前
Rust 异步编程实践:从 Tokio 基础到阻塞任务处理模式
后端·rust·github
然我20 小时前
React 开发通关指南:用 HTML 的思维写 JS🚀🚀
前端·react.js·html
萧曵 丶21 小时前
Rust 中的返回类型
开发语言·后端·rust
浪裡遊1 天前
Sass详解:功能特性、常用方法与最佳实践
开发语言·前端·javascript·css·vue.js·rust·sass
马特说1 天前
React金融数据分析应用性能优化实战:借助AI辅助解决18万数据量栈溢出Bug
react.js·金融·数据分析
归于尽1 天前
useEffect玩转React Hooks生命周期
前端·react.js