原文连接:https://overreacted.io/how-does-setstate-know-what-to-do/ by Dan Abramov
复制代码
在组件中调用setState
时,你认为会发生什么?html
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 }
而且会返回<h1>Thanks</h1>
元素以更新DOM
。react
看起来很直接,可是等等,这是React的功劳仍是React DOM呢?git
更新DOM
听起来像是React DOM
负责的。可是咱们调用了this.setState()
,不是从React DOM
中来的。咱们的React.Component
基础类是在React本身内部定义的。github
所以React.Component
中的setState()
是如何更新DOM
的?编程
免责声明:就像博客中的其余文章同样,使用React生产不须要知道这些内在的原理。这篇博客是为了那些想要知道幕后的东西的人而写的。彻底可选的!react-native
咱们可能会认为React.Component
类包括DOM的更新逻辑。api
但若是是那种状况,this.setState()
是如何能在其余环境下工做的呢?例如React Native 应用的组件也是继承自React.Component
。跟上面同样,他们也是那样调用this.setState()
,然而React Native使用的是Android和iOS原生视图,而不是DOM。数组
你可能还熟悉React Test Renderer 或 Shallow Renderer。这两种测试策略都容许你渲染普通组件而且调用其中的this.setState()
。但它们都不能和DOM一块儿工做。dom
若是你使用React ART同样的渲染器,你可能也知道在页面中使用多个渲染器是可能的。(例如,ART组件在React DOM树的内部工做。)这使得全局标志或者变量站不住脚。ide
所以在某种程度上React.Component
将状态更新的处理委托给特定平台的程序处理。在咱们了解这是怎样发生的以前,让咱们深刻到包是如何分离的以及其中的缘由去。
有一个常见的误解,认为React“引擎”驻留在React包中。这不是真的。
事实上,自从包在React 0.14中分裂后,react
包试图只暴露用于定义组件的APIs。大多数React的实现都存在于“renderers”中。
react-dom
,react-dom/server
,react-native
,react-test-renderer
,react-art
是一些渲染器的例子(你也能够建立你本身的)
这就是react
包无论针对哪一个平台都有用的缘由。全部导出,例如React.Component
,React.createElement
,React.Children
实例与(最终的)Hooks,都独立于目标平台。不管你运行React DOM
、React DOM Server
仍是React Native
,组件都将以相同的方式导入和使用它们。
造成鲜明比较的是,renderer
包公开了特定于平台的api
,好比ReactDOM.render()
,它容许你将一个React层次结构挂载到DOM节点中。每个renderer
都提供一个相似的API。理想状态下,大多数组件不必从renderer
中导入任何东西。这使它们更轻便。
大多数人想象每一个renderer
中都存在React“engine”。许多renderers
都包含相同代码的副本——咱们称其为“reconciler”。构建步骤将协调(reconciler)程序代码与渲染器(renderer)程序代码合并到一个高度优化的包中,以得到更好的性能。(复制代码一般对于包尺寸来讲不是很好,可是大多数React用户一次只须要一个渲染器,例如react-dom
.)
这里的要点是react包只容许你使用react特性,可是不知道它们是如何实现的。renderer
程序包(react-dom
, react-native
等等)提供了React特性和特定平台的逻辑的实现。其中一些代码是共享的(“reconciler”),但这是单个renderers
程序的实现细节。
如今咱们知道了为何须要为新特性更新react
和react-dom
包。例如,当React 16.3中添加了Context API
后,React.createContext()
就在React包中被暴露出来。
可是React.createContext()
没有真正实现context特性。例如,在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.Comsumer>
时,是renderer
决定如何处理它们。React DOM
可能以一种方式跟踪context
的值,但React DOM Server
可能采用不一样的方式。
所以,若是你更新react
到16.3+,但不更新react-dom
,那么你将使用一个没有意识到特定的Provider
和Consumer
类型的renderer
。这就是为何旧的react-dom
没法说这些类型是失效的。
一样的警告能够运用到React Native
中。然而,不像React DOM
,React
的版本发布不会当即“强制”React Native
的版本发布。它们有独立的版本发布表。更新后的renderer
程序代码每隔几周就会被单独同步到React
本地存储库中一次。这就是为何React Native
中的特性与React DOM
中的特性在时间表上有所不一样的缘由。
好了,如今咱们知道了react
包没有包含任何有趣的东西,而且它的实现都存在与renderers
中像react-dom
,react-native
等等。可是这并无回答咱们的问题。React.Component
中的setState()
是如何和正确的renderer
“交谈”的?
**答案是每一个renderer
在建立的类上设置一个特殊的字段。**这个特殊字段被称做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
在React.Component
中的实现,它所作的只是将工做委托给建立这个组件实例的renderer
:
// 简化版
setState(partialState, callback) {
// 使用`updater`与渲染器对话
this.updater.enqueueSetState(this, partialState, callback);
}
复制代码
React DOM Server可能想要忽略状态更新而且警告你,而React DOM和React Native会让它们的协调器副原本处理它。
这就是this.setstate()如何更新DOM的,即便它是在React包中定义的。但它读取的React DOM
设置的this.updater
,而且让React DOM
进行调度和处理更新。
咱们如今知道类(Class)了,可是钩子(Hooks)呢?
当人们第一次看到Hooks API的提议时,他们常常会想:useState
是如何知道要作什么的?觉得它比基于基础React.Component
类的this.setState()
更加神奇。
可是就像咱们今天所看到的,基础类的setState()
实现一直都是幻觉。它没有作任何事情除了将调用转到当前的renderer
中。useState
也作了一样的事情。
取代了updater
字段,Hooks使用了“dispatcher”对象。当你调用React.useState()
, React.useEffect()
,或者其它被建立的钩子时,这些调用会转向到当前的dispatcher
中。
// 在React中(简化版)
const React = {
// 真正的属性被藏得有点深,若是你能找到它的话就看一下
_currentDispatcher: null,
useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},
useEffect(initialState) {
return React.__currentDispatcher.useEffect(initialState);
},
// ...
}
复制代码
特定的renderer
在你的组件渲染以前就设置了dispatcher:
// React DOM 中
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;
let result;
try {
result = YourComponent(props);
} finally {
// 恢复过来
React.__currentDispatcher = prevDispatcher;
}
复制代码
例如,React DOM Server这里的实现,和这里的被React DOM和React Native共享的调节器的实现。
这就是为何像react-dom
这样的renderer
程序须要访问调用钩子的同一个react包。不然,你的组件不会“看到”dispatcher!当你在同一个组件树中有多个React副本时,这可能没法工做。然而,这老是会致使一些模糊的bug,所以Hooks
迫使你在付出代价以前解决包复制问题。
虽然咱们不鼓励这样作,可是对于高级工具用例,你能够重写dispatcher
。(我在__currentDispatcher
名称上撒谎了,可是你能够在React repo
中找到真正的名称。)例如,React DevTools
将使用一个特殊的专门构建的dispatcher经过捕获JavaScript
堆栈跟踪来反映Hooks树。别在家里重复作这件事。
这也意味着钩子自己并不与react
绑定。若是未来有更多的库但愿重用相同的原语钩子,理论上,dispatcher
能够转移到一个单独的包中,并做为一个一流的没有那么可怕的名称的API公开。在实践中,咱们但愿在须要抽象以前避免过早地抽象。
updater
字段和__currentDispatcher
对象都是依赖注入的通用编程原则的形式。在这两种状况下,renderer
程序都将setState
等特性的实现“注入”到通用的React包中,以使组件更具声明性。
当你使用React时你不须要考虑这是怎样工做的。咱们但愿用户花更多的时间思考他们的应用程序代码,而不是像依赖注入
这样的抽象概念。可是若是你曾经想了解 this.setstate()
或useState()
如何知道该作什么的,我但愿这能有所帮助。