当你在组件中调用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 (
<buttononClick={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 DOM
的东西。 而且我们组件的父类React.Component
也是在React
本身定义的。
所以存在于React.Component
内部的setState()
是如何更新DOM
的呢?
其实你不需要知道其中的任何知识,就可以有效地使用React。本文面向的是那些想要了解React背后原理的人。而这完全是可选的!
我们或许会认为:React.Component
类包含了DOM
更新的逻辑。
但是如果是这样的话,this.setState()
又如何能在其他环境下使用呢?
举个例子,React Native app
中的组件也是继承自React.Component
。
他们依然可以像我们在上面做的那样调用this.setState()
而且React Native
渲染的是安卓和iOS原生的界面而不是DOM
。
因此,React.Component
以某种未知的方式将处理状态(state
)更新的任务委托给了特定平台的代码。
在我们理解这些是如何发生的之前,让我们深挖一下包(packages
)是如何分离的以及为什么这样分离。
有一个很常见的误解就是React“引擎”
是存在于react
包里面的。 然而事实并非如此。
react
包故意只暴露一些定义组件的API
。绝大多数React
的实现都存在于“渲染器(renderers)”
中
react-dom
、react-dom/server
、 react-native
、 react-test-renderer
、 react-art
都是常见的渲染器(当然你也可以创建属于你的渲染器)。
这就是为什么不管你的目标平台是什么,react
包都是可用的。从react
包中导出的一切,比如React.Component
、React.createElement
、 React.Children
和(最终的)Hooks
,都是独立于目标平台的。
无论你是运行React DOM
,还是 React DOM Server
,或是 React Native
,你的组件都可以使用同样的方式导入和使用。
相比之下,渲染器包暴露的都是特定平台的API
,比如说:ReactDOM.render()
,可以让你将React
层次结构(hierarchy)挂载进一个DOM
节点。每一种渲染器都提供了类似的API
。
理想状况下,绝大多数组件都不应该从渲染器中导入任何东西。只有这样,组件才会更加灵活。
和大多数人现在想的一样,React “引擎”就是存在于各个渲染器的内部。
很多渲染器包含一份同样代码的复制 —— 我们称为“协调器”(“reconciler”)。
构建步骤(build step
)将协调器代码和渲染器代码平滑地整合成一个高度优化的捆绑包(bundle)以获得更高的性能
(代码复制通常来说不利于控制捆绑包的大小,但是绝大多数React用户同一时间只会选用一个渲染器,比如说react-dom
。)
这里要注意的是: react
包仅仅是让你使用 React
的特性,但是它完全不知道这些特性是如何实现的。
而渲染器包(react-dom、react-native等)
提供了React
特性的实现以及平台特定的逻辑。
这其中的有些代码是共享的(“协调器
”),但是这就涉及到各个渲染器的实现细节了。
现在我们知道为什么当我们想使用新特性时,react
和 react-dom
都需要被更新。
举个例子,当React 16.3
添加了Context API
,React.createContext()API
会被React
包暴露出来。
但是React.createContext()
其实并没有实现 context
。
因为在React DOM
和 React DOM Server
中同样一个 API
应当有不同的实现。所以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
也许用某种方式追踪context
的值,但是React DOM Server
用的可能是另一种不同的方式。
所以,如果你将react
升级到了16.3+
,但是不更新react-dom
,那么你就使用了一个尚不知道Provider
和 Consumer
类型的渲染器。这就是为什么一个老版本的react-dom
会报错说这些类型是无效的。
好吧,所以现在我们知道了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;
查看 React.Component中setState的实现, setState
所做的一切就是委托渲染器创建这个组件的实例:
// 适当简化的代码
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
React DOM Server
也许想 忽略一个状态更新并且警告你,而React DOM
与 React Native
却想要让他们协调器(reconciler
)的副本处理它。
这就是this.setState()
尽管定义在React
包中,却能够更新DOM
的原因。它读取由React DOM
设置的this.updater
,让React DOM
安排并处理更新。
现在关于类的部分我们已经知道了,那关于Hooks
的呢?
当人们第一次看见Hooks API,他们可能经常会想: useState
是怎么 “知道要做什么”的?然后假设它比那些包含this.setState()
的React.Component
类更“神奇”。
但是正如我们今天所看到的,基类中setState()
的执行一直以来都是一种错觉。
它除了将调用转发给当前的渲染器外,什么也没做。useState Hook
也是做了同样的事情。
Hooks
使用了一个“dispatcher
”对象,代替了updater
字段。
当你调用React.useState()
、React.useEffect()
、 或者其他内置的Hook
时,这些调用被转发给了当前的dispatcher
。
// React内部(适当简化)
const React = {
// 真实属性隐藏的比较深,看你能不能找到它!
__currentDispatcher: null,
useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},
useEffect(initialState) {
return React.__currentDispatcher.useEffect(initialState);
},
// ...
};
各个渲染器会在渲染你的组件之前设置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
这样的渲染器需要访问那个你调用Hooks
的react
包。
否则你的组件将不会“看见”dispatcher
!如果在一个组件树中存在React
的多个实例,也许并不会这样。
但是,这总是导致了一些模糊的错误,因此Hooks
会强迫你在出现问题之前解决包的重复问题。
在高级工具用例中,你可以在技术上覆盖dispatcher
,尽管react
不鼓励这种操作。(对于__currentDispatcher
这个名字我撒谎了,但是你可以在React
仓库中找到真实的名字。
比如说, React DevTools
将会使用一个专门定制的dispatcher
通过捕获JavaScript
堆栈跟踪来观察Hooks
树。请勿模仿。
这也意味着Hooks
本质上并没有与React
绑定在一起。
如果未来有更多的库想要重用同样的原生的Hooks
, 理论上来说dispatcher
可以移动到一个分离的包中,然后暴露成一个一等(first-class)的API
,然后给它起一个不那么“吓人”的名字。但是在实践中,我们会尽量避免过早抽象,直到需要它为止。
updater
字段和__currentDispatcher
对象都是称为依赖注入的通用编程原则的形式。
在这两种情况下,渲染器将诸如setState
之类的功能的实现“注入”到通用的React
包中,以使组件更具声明性。
使用React
时,你无需考虑这其中的原理。react
希望React
用户花更多时间考虑他们的应用程序代码,而不是像依赖注入这样的抽象概念。
但是如果你想知道this.setState()
或useState()
是如何知道该做什么的,我希望这篇文章会有所帮助。