# 1 引言

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

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

通过 [how-does-setstate-know-what-to-do](https://overreacted.io/how-does-setstate-know-what-to-do/) 这篇文章，可以解开这个秘密。

# 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](https://github.com/facebook/react/tree/master/packages/react-reconciler)，通过这个链接可以看到 `react-dom` `react-native` `react-art` 这三个包的 reconciler 实现。

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

对于 `context`，react 包仅仅会做如下定义：

```js
// 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` 版本的话，就可能碰到这样的报错：[fail saying these types are invalid](https://stackoverflow.com/questions/49662743/context-components-in-react-16-3-are-invalid/49677020#49677020)，原因是 API 定义与实现不匹配。

## setState 怎么调用平台实现

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

```js
// 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` 时调用的：

```js
// 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` 时，其内部调用如下：

```js
// 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`（简化的说法）:

```js
// 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` 就重写了 [dispatcher](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-debug-tools/src/ReactDebugHooks.js#L203-L214)。

由于需要同时实现 `readContext`, `useCallback`, `useContext`, `useEffect`, `useImperativeMethods`, `useLayoutEffect`, `useMemo`, `useReducer`, `useRef`, `useState`，工程量比较浩大，建议了解基本架构就足够了，除非你要深入参与 React 生态建设。

# 3 精读

与其他 React 分析文章不同，本文并没有过于刨根问题的上来就剖析 [reconciler](https://github.com/facebook/react/tree/master/packages/react-reconciler) 实现，而是问了一个最基本的疑问：为什么 `setState` 来自 `react` 包，但实现却在 `react-dom` 里？React 是如何实现这个 magic 的？

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

## 接口的力量

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

### UI 组件跨三端的接口

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

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

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

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

### 通用数据查询服务

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

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

### 小程序融合方案

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

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

# 4 总结

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

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

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

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

> 讨论地址是：[精读《setState 做了什么》 · Issue #122 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/122)

**如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
