从零实现 React v18,但 WASM 版 - [8] 支持 Hooks

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

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

本文对应 tag:v8

上篇文章实现了对 FunctionComponent 类型的支持,但是还不支持 Hooks,这篇文章我们以 useState 为例,来介绍如何实现。

不知道经常使用 react 的你有没有过这样的疑问:useState 是从 react 库里面引入的,但是 useState 的具体实现则是在 react-reconciler 中,那是怎么做到的呢?react 依赖了 react-reconciler

为了搞清楚这个问题,我们先来分析下 big-react。

首先看下 useState 的入口文件:

ts 复制代码
// react/index.ts
import currentDispatcher, {
	Dispatcher,
	resolveDispatcher
} from './src/currentDispatcher';

export const useState = <State>(initialState: (() => State) | State) => {
	const dispatcher = resolveDispatcher() as Dispatcher;
	return dispatcher.useState<State>(initialState);
};

export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
	currentDispatcher
};

// react/src/currentDispatcher.ts
...
const currentDispatcher: { current: null | Dispatcher } = {
	current: null
};

export const resolveDispatcher = () => {
	const dispatcher = currentDispatcher.current;

	if (dispatcher === null) {
		console.error('resolve dispatcher时dispatcher不存在');
	}
	return dispatcher;
};

export default currentDispatcher;

代码很简单,执行 useState 时,核心逻辑为调用 currentDispatcher.current 上的 useState 方法。很明显,currentDispatcher.current 初始化是 null,那么它在哪里进行赋值的呢?答案是在 renderWithHooks 中:

js 复制代码
// react-reconciler/src/fiberHooks.ts
export const renderWithHooks = (workInProgress: FiberNode) => {
  ...
  currentDispatcher.current = HooksDispatcherOnMount
  ...
}

并且这里的 currentDispatcher 还不是直接从 react 导入的,而是从 shared 这个库导入,而 shared 最后从 react 中导入了 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,它包含 currentDispatcher 属性:

js 复制代码
// react-reconciler/src/fiberHooks.ts
import sharedInternals from 'shared/internals'
const {currentDispatcher} = sharedInternals

// shared/internals.ts
import * as React from 'react'
const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
export default internals

// react/index.ts
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
  currentDispatcher,
}

所以就形成了这样一个依赖关系:

vbnet 复制代码
react-dom ---depend on--> react-reconciler ---depend on--> shared ---depend on--> react

打包时,reactshared 打包成一个 react.js,而打包 react-dom 时需要指定 react 为 external, 这样打包出来的 react-dom.js 中不会包含 react 的代码,而是作为外部依赖:

ini 复制代码
react + shared => react.js
react-dom + react-reconciler + shared => react-dom.js

这样的好处是可以方便的替换 Renderer,比如后续要实现用于单测的 react-noop

ini 复制代码
react-noop + react-reconciler + shared => react-noop.js

但是 WASM 构建明显是不支持 external 的,怎么办呢?重新思考下,发现要实现上面的要求,核心在于两点:

  • react 和 renderer 代码要分开打包
  • 要让 renderer 去依赖 react,并能够在运行时修改 react 中的变量的值

其中分开打包现在我们已经实现了,现在要实现第二点,也就是要实现一个 WASM 模块修改另一个 WASM 模块中的变量的值。查阅 wasm-bindgen 的文档,发现除了 WASM 可以导出方法给 JS 使用外,也可以从 JS 中导入方法给 WASM 来调用:

rust 复制代码
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
  fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
  // the alert is from JS
  alert(&format!("Hello, {}!", name));
}

所以,我们可以通过 JS 作为中转来实现一个 WASM 模块修改另一个 WASM 模块中的变量的值。具体做法如下:

我们在 react 中导出一个 updateDispatcher 方法给 JS,用于更新 react 中的 CURRENT_DISPATCHER.current

rust 复制代码
fn derive_function_from_js_value(js_value: &JsValue, name: &str) -> Function {
    Reflect::get(js_value, &name.into()).unwrap().dyn_into::<Function>().unwrap()
}

#[wasm_bindgen(js_name = updateDispatcher)]
pub unsafe fn update_dispatcher(args: &JsValue) {
    let use_state = derive_function_from_js_value(args, "use_state");
    CURRENT_DISPATCHER.current = Some(Box::new(Dispatcher::new(use_state)))
}

然后,我们在 react-reconciler 中声明对这个方法的导入(这里简单起见没有再从 shared 中导入了):

rust 复制代码
#[wasm_bindgen]
extern "C" {
    fn updateDispatcher(args: &JsValue);
}

render_with_hooks 时,会调用 updateDispatcher,传入一个包含 use_state 属性的 Object

rust 复制代码
fn update_mount_hooks_to_dispatcher() {
    let object = Object::new();

    let closure = Closure::wrap(Box::new(mount_state) as Box<dyn Fn(&JsValue) -> Vec<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());
}

最后,我们需要在打包出来的 react-dom/index_bg.js 顶部插入一段代码,从 react 中引入 updateDispatcher 这个方法:

js 复制代码
import {updateDispatcher} from 'react'

当然,这一步可以写一个脚本来实现。

总结下来,上面的流程可以简单表示为:

本次的更新详见这里

我们来测试下,修改一下 hello-world 的例子:

ts 复制代码
import {useState} from 'react'

function App() {
  const [name, setName] = useState(() => 'ayou')
  setTimeout(() => {
    setName('ayouayou')
  }, 1000)
  return (
    <div>
      <Comp>{name}</Comp>
    </div>
  )
}

function Comp({children}) {
  return (
    <span>
      <i>{`Hello world, ${children}`}</i>
    </span>
  )
}

export default App

结果如下所示:

很奇怪吧?那是因为我们目前还没有完整的实现更新流程。

到此,我们已经复刻出了 big react v3 这个版本。

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

相关推荐
baiduguoyun44 分钟前
react的import 导入语句中的特殊符号
前端·react.js
幸运小圣3 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
ClareXi4 小时前
react项目通过http调用后端springboot服务最简单示例
spring boot·react.js·http
老猿讲编程4 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
yezipi耶不耶11 小时前
Rust 所有权机制
开发语言·后端·rust
喜欢打篮球的普通人11 小时前
rust并发
rust
一点一木14 小时前
WebAssembly:Go 如何优化前端性能
前端·go·webassembly
大鲤余14 小时前
Rust开发一个命令行工具(一,简单版持续更新)
开发语言·后端·rust
梦想画家14 小时前
快速学习Serde包实现rust对象序列化
开发语言·rust·序列化
咔咔库奇16 小时前
react动态路由
前端·react.js·前端框架