本文翻译自:How Does setState Know What to Do? 原作者:Dan Abramov
如果有任何版权问题,请联系 shuirong1997@icloud.com
当你在组件中调用 setState 时,你觉得会发生什么?
import React from ‘react’;
import ReactDOM from ‘react-dom’;
class Button extends React.Component {
constructor(props) {
super(props);
this.state = {clicked: false};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({clicked: true});
}
render() {
if (this.state.clicked) {
return <h1>Thanks</h1>;
}
return (
<button onClick={this.handleClick}>
Click me!
</button>
);
}
}
ReactDOM.render(<Button />, document.getElementById(‘container’));
当然,React 会用 {clicked: true} 这条状态重新渲染组件并且更新匹配到的 DOM,然后返回 <h1>Thanks</h1> 元素。
听起来似乎简洁明了。但别急,React(或者说 React DOM)是怎么做的?
更新 DOM 听起来像是 React DOM 的事儿,但别忘了我们调用的可是 this.setState(),它是 React 的东西,可不是 React DOM 的。另外,我们的基类 React.Component 是被定义在 React 内部。
所以问题来了:React.Component 内部的 setState 怎么能去更新 DOM 呢?
事先声明:就像我的其他博客,你不需要熟练掌握 React。这篇博客是为那些想要看看面纱之后是什么东西的人准备的。完全可选!
我们或许会认为 React.Component 类已经包含了 DOM 更新逻辑。
但如果这是事实,那 this.setState 是如何工作在其他环境中呢?比如:在 React Native App 中的组件也能继承 React.Component,他们也能像上面一样调用 this.setState(),并且 React Native 工作在 Android 和 iOS 的原生视图而不是 DOM 中。
你可能也对 React Test Renderer 或 Shallow Renderer 比较熟悉。这两个测试渲染器让你可以渲染一般的组件并且也能在他们中调用 this.setState,但他们可都不使用 DOM。
如果你之前使用过一些渲染器比如说 React ART,你可能知道在页面中使用超过一个渲染器是没什么问题的。(比如:ART 组件工作在 React DOM 树的内部。)这会产生一个不可维持的全局标志或变量。
所以 React.Component 以某种方式将 state 的更新委托为具体的平台(译者注:比如 Android, iOS),在我们理解这是如何发生之前,让我们对包是如何被分离和其原因挖得更深一点吧!
这有一个常见的错误理解:React “ 引擎 ” 在 react 包的内部。这不是事实。
事实上,从 React 0.14 开始对包进行分割时,React 包就有意地仅导出关于如何定义组件的 API 了。React 的大部分实现其实在“渲染器”中。
渲染器的其中一些例子包括:react-dom,react-dom/server,react-native,react-test-renderer,react-art(另外,你也可以构建自己的)。
这就是为什么 react 包帮助很大而不管作用在什么平台上。所有它导出的模块,比如 React.Component,React.createElement,React.Children 和 [Hooks](https://reactjs.org/docs/hooks-intro.html),都是平台无关的。无论你的代码运行在 React DOM、React DOM Server、还是 React Native,你的组件都可以以一种相同的方式导入并且使用它们。
与之相对的是,渲染器会暴露出平台相关的接口,比如 ReactDOM.render(),它会让你可以把 React 挂载在 DOM 节点中。每个渲染器都提供像这样的接口,但理想情况是:大多数组件都不需要从渲染器中导入任何东西。这能使它们更精简。
大多数人都认为 React“引擎”是位于每个独立的渲染器中的。许多渲染器都包含一份相同的代码—我们叫它“调节器”,为了表现的更好,遵循这个步骤 可以让调节器的代码和渲染器的代码在打包时归到一处。(拷贝代码通常不是优化“打包后文件”(bundle)体积的好办法,但大多数 React 的使用者一次只需要一个渲染器,比如:react-dom(译者注:因此可以忽略调节器的存在))
The takeaway here 是 react 包仅仅让你知道如何使用 React 的特性而无需了解他们是如何被实现的。渲染器(react-dom,react-native 等等)会提供 React 特性的实现和平台相关的逻辑;一些关于调节器的代码被分享出来了,但那只是单独渲染器的实现细节而已。
现在我们知道了为什么 react 和 react-dom 包需要为新特定更新代码了。比如:当 React16.3 新增了 Context 接口时,React.createContext() 方法会在 React 包中被暴露出来。
但是 React.createContext() 实际上不会实现具体的逻辑(译者注:只定义接口,由其他渲染器来实现逻辑)。并且,在 React DOM 和 React DOM Server 上实现的逻辑也会有区别。所以 createContext() 会返回一些纯粹的对象(定义如何实现):
// 一个简单例子
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;
}
你会在某处代码中使用 <MyContext.Provider> 或 <MyContext.Consumer>,那里就是决定着如何处理他们的渲染器。React DOM 会用 A 方法追踪 context 值,但 React DOM Server 或许会用另一个不同的方法实现。
所以如果你将 react 升级到 16.3+,但没有升级 react-dom,你将使用一个还不知道 Provider 和 Consumer 类型的渲染器,这也就旧版的 react-dom 可能会报错:fail saying these types are invalid 的原因。
同样的警告也会出现在 React Native 中,但是不同于 React DOM,一个新的 React 版本不会立即产生一个对应的 React Native 版本。他们(React Native)有自己的发布时间表。大概几周后,渲染器代码才会单独更新到 React Native 库中。这就是为什么新特性在 React Native 生效的时间会和 React DOM 不同。
Okay,那么现在我们知道了 react 包不包含任何好玩的东西,并且具体的实现都在像 react-dom,react-native 这样的渲染器中。但这并不能回答我们开头提出的问题。React.Component 里的 setState() 是如何和对应的渲染器通信的呢?
答案是每个渲染器都会在创建的类中添加一个特殊的东西,这个东西叫 updater。它不是你添加的东西—恰恰相反,它是 React DOM,React DOM Server 或者 React Native 在创建了一个类的实例后添加的:
// React DOM 中是这样
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;
// React DOM Server 中是这样
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;
// React Native 中是这样
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
从 setState 的实现就可以看出,它做的所有的工作就是把任务委托给在这个组件实例中创建的渲染器:
// 简单例子
setState(partialState, callback) {
// 使用 `updater` 去和渲染器通信
this.updater.enqueueSetState(this, partialState, callback);
}
React DOM Server 可能想忽略状态更新并且警告你,然而 React DOM 和 React Native 将会让调节器的拷贝部分去 处理它。
这就是尽管 this.setState() 被定义在 React 包中也可以更新 DOM 的原因。它调用被 React DOM 添加的 this.updater 并且让 React DOM 来处理更新。
现在我们都比较了解“类”了,但“钩子”(Hooks)呢?
当人们第一次看到 钩子接口的提案时,他们常回想:useState 是怎么知道该做什么呢?这一假设简直比对 this.setState() 的疑问还要迷人。
但就像我们如今看到的那样,setState() 的实现一直以来都是模糊不清的。它除了传递调用给当前的渲染器外什么都不做。所以,useState 钩子做的事也是如此。
这次不是 updater,钩子(Hooks)使用一个叫做“分配器”(dispatcher)的对象,当你调用 React.useState()、React.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);
},
// …
};
单独的渲染器会在渲染你的组件之前设置分配器(dispatcher)。
// In React DOM
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;let result;
try {
result = YourComponent(props);
} finally {
// Restore it back React.__currentDispatcher = prevDispatcher;}
React DOM Server 的实现在这里。由 React DOM 和 React Native 共享的调节器实现在这里。
这就是为什么像 react-dom 这样的渲染器需要访问和你调用的钩子所使用的 react 一样的包。否则你的组件将找不到分配器!如果你有多个 React 的拷贝在相同的组件树中,代码可能不会正常工作。然而,这总是造成复杂的 Bug,因此钩子会在它耗光你的精力前强制你去解决包的副本问题。
如果你不觉得这有什么,你可以在工具使用它们前精巧地覆盖掉原先的分配器(__currentDispatcher 的名字其实我自己编的但你可以在 React 仓库中找到它真正的名字)。比如:React DevTools 会使用一个特殊的内建分配器来通过捕获 JavaScript 调用栈来反映(introspect)钩子。不要在家里重复这个(Don’t repeat this at home.)(译者注:可能是“不要在家里模仿某项实验”的衍生体。可能是个笑话,但我 get 到)
这也意味着钩子不是 React 固有的东西。如果在将来有很多类库想要重用相同的基础钩子,理论上来说分配器可能会被移到分离的包中并且被塑造成优秀的接口—会有更少让人望而生畏的名称—暴露出来。在实际中,我们更偏向去避免过于仓促地将某物抽象,直到我们的确需要这么做。
updater 和__currentDispatcher 都是泛型程序设计(依赖注入 /dependency injection)的绝佳实例。渲染器“注入”特性的实现。就像 setState 可以让你的组件看起来简单明了。
当你使用 React 时,你不需要考虑它是如何工作的。我们期望 React 用户去花费更多的时间去考虑它们的应用代码而不是一些抽象的概念比如:依赖注入。但如果你曾好奇 this.setState() 或 useState() 是怎么知道它们该做什么的,那我希望这篇文章将帮助到你。