从零实现 React v18,但 WASM 版 - [16] 实现 React Noop

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

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

本文对应 tag:v16

之前的文章总是在说要实现 React Noop 用于单元测试,今天就来完成这个任务。

首先,我们按照之前的方式,在 react-dom 同级目录下新建一个 react-noop:

vbnet 复制代码
├── packages
│   ├── react
│   ├── react-dom
│   ├── react-noop
│   ├── react-reconciler
│   ├── scheduler
│   └── shared

项目结构与 react-dom 类似,不同之处在于 react-noop 对于 HostConfig 的实现方式不同。比如 react-dom 中的 create_instance 返回的是一个 Element 对象:

rust 复制代码
fn create_instance(&self, _type: String, props: Rc<dyn Any>) -> 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) => {
          let element = update_fiber_props(
              element.clone(),
              &*props.clone().downcast::<JsValue>().unwrap(),
          );
          Rc::new(Node::from(element))
      }
      Err(_) => {
          panic!("Failed to create_instance {:?}", _type);
      }
  }
}

而 react-noop 返回的是一个普通的 JS 对象:

rust 复制代码
fn create_instance(&self, _type: String, props: Rc<dyn Any>) -> Rc<dyn Any> {
  let obj = Object::new();
  Reflect::set(&obj, &"id".into(), &getCounter().into());
  Reflect::set(&obj, &"type".into(), &_type.into());
  Reflect::set(&obj, &"children".into(), &**Array::new());
  Reflect::set(&obj, &"parent".into(), &JsValue::from(-1.0));
  Reflect::set(
      &obj,
      &"props".into(),
      &*props.clone().downcast::<JsValue>().unwrap(),
  );
  Rc::new(JsValue::from(obj))
}

其他方法也都是对普通 JS 对象的操作而已,具体请看这里

另外,为了方便测试,还需要新增一个 getChildrenAsJSX 的方法:

rust 复制代码
impl Renderer {
    ...
    pub fn getChildrenAsJSX(&self) -> JsValue {
        let mut children = derive_from_js_value(&self.container, "children");
        if children.is_undefined() {
            children = JsValue::null();
        }
        children = child_to_jsx(children);

        if children.is_null() {
            return JsValue::null();
        }
        if children.is_array() {
            todo!("Fragment")
        }
        return children;
    }
}

这样就可以通过 root 来得到一颗包含 JSX 对象的树状结构了,比如下面的代码:

js 复制代码
const ReactNoop = require('react-noop')
const root = ReactNoop.createRoot()
root.render(
  <div>
    <p>hello</p>
    <span>world</span>
  </div>
)
setTimeout(() => {
  console.log('---------', root.getChildrenAsJSX())
}, 1000)

最终打印的结果会是:

js 复制代码
{
  $$typeof: 'react.element',
  type: 'div',
  key: null,
  ref: null,
  props: {
    children: [
      {
        $$typeof: 'react.element',
        type: 'p',
        key: null,
        ref: null,
        props: {
          children: 'hello',
        },
      },
      {
        $$typeof: 'react.element',
        type: 'span',
        key: null,
        ref: null,
        props: {
          children: 'world',
        },
      },
    ],
  },
}

注意到上面打印结果的代码放在了 setTimeout 中,是因为我们在实现 Batch Update 的时候把更新流程放在了宏任务中,可参考这篇文章

然后,我们把 react-noop 也加入到构建脚本中,并设置构建 target 为 nodejs,这样我们就能在 Node.js 环境中使用了。不过要想在 Node.js 中支持 jsx 语法,还得借助 babel,这里我们直接使用 babel-node 来运行我们的脚本即可,并配置好相关的 preset:

js 复制代码
// .babelrc
{
  "presets": [
    [
      "@babel/preset-react",
      {
        "development": "true"
      }
    ]
  ]
}

不出意外的话,上面的代码就可以正常运行在 Node.js 中了。不过,当我尝试在 jest 中使用 react-noop 时,却运行出错:

js 复制代码
work_loop error JsValue(RuntimeError: unreachable
    RuntimeError: unreachable
        at null.<anonymous> (wasm://wasm/00016f66:1:14042)
        ...

由于一直无法解决,所以最后不得不在 Node.js 中来进行单元测试,下面是一个用例:

js 复制代码
async function test1() {
  const arr = []

  function Parent() {
    useEffect(() => {
      return () => {
        arr.push('Unmount parent')
      }
    })
    return <Child />
  }

  function Child() {
    useEffect(() => {
      return () => {
        arr.push('Unmount child')
      }
    })
    return 'Child'
  }

  root.render(<Parent a={1} />)
  await sleep(10)
  if (root.getChildrenAsJSX() !== 'Child') {
    throw new Error('test1 failed')
  }

  root.render(null)
  await sleep(10)
  if (arr.join(',') !== 'Unmount parent,Unmount child') {
    throw new Error('test1 failed')
  }
}

执行 test1 成功,说明我们的 React Noop 可以正常工作了。

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

相关推荐
得物技术7 分钟前
基于IM场景下的Wasm初探:提升Web应用性能|得物技术
rust·web·wasm
PleaSure乐事13 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
getaxiosluo13 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
老猿讲编程13 小时前
用示例来看C2Rust工具的使用和功能介绍
rust
金庆13 小时前
How to set_default() using config-rs crate
rust·config·set_default·valuekind
新星_14 小时前
函数组件 hook--useContext
react.js
阿伟来咯~15 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端15 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱15 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
许野平15 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono