react的setState如何知道他要干什么
原文: How Does setState Know What to Do?
译:可能看到标题的时候会想,怎么去做还不是看代码吗?react中的setState
不就是负责更新状态码?抱着好奇心看下去了。
当你在组件中调用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}
状态的时候re-render
组件并且更新DOM
去返回<h1>Thanks</h1>
元素。
看起来很简单,对吧?等一下,请考虑下,这个是react
在处理?或者是ReactDOM
在处理?
更新DOM
听起来似乎是React DOM
在负责处理。但是我们调用的是this.setState
,这个api
是来自react
,并非是React DOM
。并且我们的React.Component
是定义在React
里的。
所以React.Component.prototype.setState()
是如何去更新DOM
的。
事先声明: 就像本博客大多数的其他的文章一样,你其实可以完全不用知道这些内容,一样可以很好的使用react
。这系列的文章是针对于那些好奇react
内部原理的一些人。所以读不读,完全取决于你。
我们可能会认为React.Component
里包含了DOM
的一些更新逻辑。
但是如果是我们猜想的这样,那么this.setState()
如何在其他环境中正常工作?比如在React Native中的组件也是继承于React.Component
, React Native应用像上面一样调用this.setState()
,但React Native可以使用Android和iOS原生的视图而不是DOM。
再比如,你可能也熟悉React Test Renderer或Shallow Renderer。这些都可以让你渲染普通组件并在其中调用this.setState()
。但是他们都不适用于DOM
。
如果你使用过像React ART这样的渲染器,你便会知道可以在页面上使用多个渲染器。(例如,ART 组件在React DOM
中工作)。这使得全局标志或变量无法工作。
所以React.Component
以某种方式委托处理状态更新到特定的平台。 在我们理解这是如何发生之前,让我们深入了解包的分离方式和原因。
有一种常见的误区就是React
“引擎(engine)”存在React
包中。这其实是不对的。
事实上,自从React 0.14拆分包以来,react包只是暴露了用于定义组件的API。React的大多数实现都在“渲染器(renderers)”中。
react-dom
,react-dom / server
,react-native
,react-test-renderer
,react-art
是在renderers
中的一些例子(你也可以建立属于你自己的)。
所以,无论在什么平台,react
包都可以正常工作。他对外暴露的所有的内容,例如: React.Component
,React.createElement
,React.Children
的作用和Hook,都独立于目标平台。无论运行React DOM
,React DOM Server
还是React Native
,组件都将以相同的方式导入和使用它们。
相比之下,renderer
包对外暴露了特定于平台的api
,像ReactDOM.render
就可以让你把React
的层次结构挂载到DOM
节点。每个renderer
都提供了像这样的API
。理想情况下,大多数组件不需要从renderer
导入任何内容。这使它们更轻便易用。
大多数人都认为react的"引擎(engine)"在每个renderer
中。 许多renderer
都包含相同代码的副本 - 我们将其称为“reconciler(和解)”。构建步骤将reconciler(和解)的代码与renderer(渲染器)代码一起成为一个高度优化的捆绑包,以获得更好的性能。(复制代码对于包大小通常不是很好,但绝大多数React用户一次只需要一个渲染器,例如react-dom。)
这里要说的是,react包只允许你使用React功能,但不知道它们是如何实现的。renderer
包(react-dom
,react-native
等)提供了React功能和特定于平台的逻辑的实现。其中一些代码是共享的(“reconciler”),但这是各个渲染器的实现细节。
现在我们知道了为什么每次有新的功能都会同时更新react
和react-dom
包。例如,当React 16.3
添加了Context API
时,React.createContext()
在React包上对外暴露。
但是React.createContext
实际上并没有实现上下文功能。例如,React DOM
和React DOM Server
之间的实现需要有所不同。所以createContext()
返回一些普通对象:
// 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;
}
当你在代码中使用<MyContext.Provider>
或者 <MyContext.Consumer>
时,这就由renderer
去决定如何处理他们。React DOM
可能以一种方式跟踪上下文值,但React DOM Server
可能会采用不同的方式。
所以如果你更新react
到16.3+,但是没有更新react dom
, 那么你使用的这个renderer
将是一个无法解析Provider
和Consumer
类型的renderer
。 这就是为什么旧的react-dom
会失败报错这些类型无效。
同样的警告也适用于React Native。但是,与React DOM不同,React的版本更新不会迫使React Native的版本去立即更新。他有一个自己的发布周期。几周后,更新过的renderer
会单独同步到React Native库中。这就是为什么React Native和React DOM可用功能的时间不一致的区别
好吧,现在我们知道了React包中不包含我们感兴趣的内容,而且这些实现是存在于像react-dom
, react-native
这样的renderer
中。但是这些并不能回答我们的问题 -- React.Component
中的setState
是如何知道他要干什么的(与对应的renderer
协同工作)。
答案是在每个创建renderer
的类上设置一个特殊的字段。 这个字段就叫做updater
。这不是你想要设置啥就设置啥,你不可以设置他,而是要在类的实例被创建后再去设置React DOM
,React DOM Server
或React Native
:
// 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;
React DOM Server 可能想要忽略状态的更新并且给你一个警告,然而React DOM和React Native会拷贝一份reconciler
(和解)代码去处理他
这就是为什么this.setState
定义在React
的包中仍然可以更新DOM
的原因。他通过读取this.updater
去获取,如果是React DOM
, 就让React DOM
调度并处理更新。
我们现在知道了类的操作方式,那么hooks
呢?
当大多数的人看到Hooks
提案的API
时,他们常常想知道: useState
是怎么'知道该去做什么’?假设他会比this.setState
更加神奇。
但是正如我们现在所看到的这个样子,对于理解setState
的实现一直是一种错觉。他除了会将调用作用到对应的renderer
之外不会再做其他任何的操作。实际上useState
这个Hook
做了同样的事情。
相对于setState
的updater
字段而言,Hooks
使用dispatcher
对象。 当调用React.useState
,React.useEffect
或其他内置的Hook
时,这些调用将转发到当前调度程序(dispatcher
)。
// 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);
},
// ...
};
并且在渲染你的组件之前,各个renderer
会设置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
共享的reconciler
实现就在这里。
这就是为什么像react-dom
这样的renderer
需要访问你调用Hooks
的同一个React包的原因。否则,你的组件将不会知道dispatcher
!当在同一组件树中有多个React副本时,这可能不会如期工作。但是,这会导致一些那难以理解的错误,所以Hook
会迫使解决包重复问题。
虽然我们不鼓励你这样做,但是对于高级工具用例,你可以在此技术上重写dispatcher
。(我对__currentDispatcher
这名称撒了谎,这个不是真正的名字,但你可以在React
库中找到真正的名字。)例如,React DevTools将使用特殊的专用dispatcher程序通过捕获JavaScript堆栈跟踪来反思Hooks
树。不要自己在家里重复这个。
这也意味着Hooks
本身并不依赖于React。如果将来有更多的库想要重用这些原始的Hook
,理论上dispatcher
可以移动到一个单独的包中,并作为一个普通名称的API对外暴露。在实践中,我们宁愿避免过早抽象,直到需要它的时候再说。
updater
字段和__currentDispatcher
对象都是一种称为依赖注入的编程原则的形式。在这些情况下,renderer
注入一些比如setState
这样的功能到React的包中,这样来保持组件更具有声明性。
在使用react
的时候,你不需要去考虑他的工作原理。我们希望React用户花更多时间考虑他们的代码而不是像依赖注入这样的抽象概念。但是如果你想知道this.setState
或useState
如何知道该怎么做,我希望本文会对你有所帮助。