1 引言

​setState​​ 是 React 框架最常用的命令,它是用来更新状态的,这也是 React 框架划时代的功能。

但是 ​​setState​​ 函数是 ​​react​​ 包导出的,他们又是如何与 ​​react-dom​​ ​​react-native​​ ​​react-art​​ 这些包结合的呢?

2 概述

​setState​​ 函数是在 ​​React.Component​​ 组件中调用的,所以最自然的联想是,更新 DOM 的逻辑在 ​​react​​ 包中实现。

但是 ​​react​​ 却可以和 ​​react-dom​​ ​​react-native​​ ​​react-art​​ 这些包打配合,甚至与 ​​react-dom/server​​ 配合在服务端运行,那可以肯定 ​​react​​ 包中不含有 DOM 更新逻辑。

所以可以推断,平台相关的 UI 更新逻辑分布在平台相关的包里,react 包只做了代理。

React 引擎不在 react 包里

从 react 0.14 版本之后,引擎代码就从 react 包中抽离了,react 包仅仅做通用接口抽象。

也就是说,react 包定义了标准的状态驱动模型的 API,而 ​​react-dom​​ ​​react-native​​ ​​react-art​​ 这些包是在各自平台的具体实现。

各平台具体的渲染引擎实现被称为 ​​reconciler​​,通过这个链接可以看到 ​​react-dom​​ ​​react-native​​ ​​react-art​​ 这三个包的 reconciler 实现。

这说明了 react 包仅告诉你 React 拥有哪些语法,而并不关心如何实现他们,所以我们需要结合 react 包与 react-xxx 一起使用。

对于 ​​context​​,react 包仅仅会做如下定义:


// A bit simplified
function createContext(defaultValue) {
let context = {
_currentValue: defaultValue,
Provider: null,
Consumer: null
};
context.Provider = {
$$typeof: Symbol.for("react.provider"),
_context: context
};
context.Consumer = {
$$typeof: Symbol.for("react.context"),
_context: context
};
return context;
}


具体用到时,由 ​​react-dom​​ 和 ​​react-native​​ 决定用何种方式实现 ​​MyContext.Provider​​ 这个 API。

这也说明了,如果你不同步升级 ​​react​​ 与 ​​react-dom​​ 版本的话,就可能碰到这样的报错:​setState 怎么调用平台实现

每个平台对 UI 更新逻辑的实现,会封装在 ​​updater​​ 函数里,所以不同平台代码会为组件添加各自的 ​​updater​​ 实现:


// Inside React DOM
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;

// Inside React DOM Server
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;

// Inside React Native
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;


不同于 ​​props​​, ​​updater​​ 无法被直接调用,因为这个 API 是由 react 引擎在 ​​setState​​ 时调用的:


// A bit simplified
setState(partialState, callback) {
// Use the `updater` field to talk back to the renderer!
this.updater.enqueueSetState(this, partialState, callback);
}


关系可以这么描述:​​react​​ -> setState -> updater <- ​​react-dom​​ 等。

Hooks

Hooks 的原理与 ​​setState​​ 类似,当调用 ​​useState​​ 或 ​​useEffect​​ 时,其内部调用如下:


// In React (simplified a bit)
const React = {
// Real property is hidden a bit deeper, see if you can find it!
__currentDispatcher: null,

useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},

useEffect(initialState) {
return React.__currentDispatcher.useEffect(initialState);
}
// ...
};


ReactDOM 提供了 ​​__currentDispatcher​​(简化的说法):


// In React DOM
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;
let result;
try {
result = YourComponent(props);
} finally {
// Restore it back
React.__currentDispatcher = prevDispatcher;
}


可以看到,Hooks 的原理与 ​​setState​​ 基本一致,但需要注意 ​​react​​ 与 ​​react-dom​​ 之间传递了 ​​dispatch​​,虽然你看不到。但这个 ​​dispatch​​ 必须对应到唯一的 React 实例,这就是为什么 Hooks 不允许同时加载多个 React 实例的原因。

和 ​​updater​​ 一样,​​dispatch​​ 也可以被各平台实现重写,比如 ​​react-debug-hooks​​ 就重写了由于需要同时实现 ​​readContext​​, ​​useCallback​​, ​​useContext​​, ​​useEffect​​, ​​useImperativeMethods​​, ​​useLayoutEffect​​, ​​useMemo​​, ​​useReducer​​, ​​useRef​​, ​​useState​​,工程量比较浩大,建议了解基本架构就足够了,除非你要深入参与 React 生态建设。

3 精读

与其他 React 分析文章不同,本文并没有过于刨根问题的上来就剖析 实现,而是问了一个最基本的疑问:为什么 ​​setState​​​ 来自 ​​react​​​ 包,但实现却在 ​​react-dom​​ 里?React 是如何实现这个 magic 的?

通过这个疑问,我们了解了 React 更上层的抽象能力,如何用一个包制定规范,用 N 包去实现它。

接口的力量

在日常编程中,接口也拥有的强大力量,下面举几个例子。

UI 组件跨三端的接口

由于 RN、Weex、Flux 的某些不足,越来越多的人选择 “一个思想三端实现” 的方式做跨三端的 UI 组件,这样既兼顾了性能,又可以照顾到平台差异性,对不同平台组件细节做定制优化。

要实施这个方案,最大问题就是接口约定。一定要保证三套实现遵循同一套 API 接口,业务代码才可以实现 “针对任意一个平台编写,自动移植到其他平台”。

比较常用的做法是,通过一套统一的 API 文件约束,固定组件的输入输出,不同平台的组件做平台具体实现。这个思想和 React 如出一辙。

当然 RN 这些框架本身也是同一接口在不同平台实现的典型,只是做的不够彻底,JS 与 Native 的通信导致了性能不如源生。

通用数据查询服务

通用数据查询服务也比较流行,通过磨平各数据库语法,让用户通过一套 SQL 查询各种类型数据库的数据。

这个方案中,一套通用的查询语法就类似 React 定义的 API,执行阶段会转化为各数据库平台的 SQL 方言。

小程序融合方案

现在这种方案很火。通过基于 template 或者 jsx 的语法,一键发布到各平台小程序应用。

这种方案一定会抽象一套通用语法,甚至几乎等价与 ​​react​​ 与 ​​react-dom​​ 的关系:所有符合规范的语法,转化为各小程序平台的实现。

4 总结

这种分平台实现方案与跨平台方案还是有很大区别的,像 JAVA 虚拟机本质还是一套实现方案。而分平台的实现可以带来最原生的性能与体验,同样收到的约束也最大,应该其 API 应该是所有平台支持的一个子集。

另外,这种方案不仅可以用于 一套规范,不同平台的实现,甚至可以用在 “同一平台的实现”。

无论是公司还是开源节界,都有许多重复的轮子或者平台,如果通过技术委员会约定一套平台的实现规范,大家都遵循这个规范开发平台,那未来就比较好做收敛,或者说收敛的第一步都是先统一 API 规范。

留下一个思考题:还有没有利用 ​​setState​​ 规范与实现分离的思想案例?欢迎留下你的答案。