原文链接:How Does setState Know What to Do?
当我们在一个组件里面调用 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元素,以匹配
似乎非常的直截了当,但是等等,是 React 做的吗?还是 React Dom 做到的?
更新DOM听起来像是 React DOM 负责做的事。但是我们调用却是 this.setState() 方法,而不是React DOM中的任何方法。并且 React.Component 这个基础类是定义在 React 中的。
所以我们想知道 到底 setState() 方法是如何更新 DOM的?
声明:即使不知道本文说的内容也可以高效的使用React。如果你想要刨根问底,那么继续吧。
我们可能会认为 React.Component 里面包含了DOM更新的逻辑。
但是如果按照这个逻辑的话,this.setState() 是如何在其他环境工作的?比如,在React Native的app中,组件也是继承React.Component。并且也是调用this.setState() 方法就如上述所言,然而React Native操纵的是Android或者IOS的界面而非DOM。
如果你用过一些渲染器,比如 React Art,那么你应该知道在同一个页面上是有可能存在多与一个的渲染器。(比如,React Art是在DOM树内部工作。)这使得认为是全局标识或者变量来控制的观点是站不住脚的。
所以React.Component以某种形式代理了具体平台相关状态处理代码逻辑。在我们深入理解之前,先来了解下React是如何分割模块(包)以及其原因所在。
通常我们会有一个误解,以为 React‘引擎’是在 React模块(包)下面的。这个其实是错误的认知。
事实上,自React 0.14版本以来,React 模块(包)有意的只暴露了定义逐渐的API。React的大多数真正的实现全都在‘渲染器’中。
下面是一些渲染器的例子:react-dom
, react-dom/server
, react-native
, react-test-renderer
, react-art
(当然你也可以创建你自己的渲染器)
这也是为什么React模块(包)在任何平台都有用的原因。对于所有暴露的API,比如React.Componet, React.createElement, React.Children等通用方法类和Hooks, 都是独立于目标平台的。不管你是使用 ReactDOM, React DOM Server 或者是 React Native,组件都是同样的方式引入和使用的。
不同的是,各个渲染器的模块回暴露平台相关的API,比如 ReactDOM.render() 可以把一个React构建组件转换为DOM节点。每一个渲染器都会提供如此的API。理想情况,任何组件都不引用任何渲染器的方法,这样可以组件更加的可移植性。
大多数人认为的React引擎是存在于每个独立的渲染器内的。许多的渲染器有包含了一段相似的代码 — 我们称之为 “ reconciler ” 。为了更好的性能,会通过一个构建过程来把 reconciler 代码和渲染器代码融合在单个个高度优化的打包文件内。(通常而言,复制代码无助于打包后文件大小的缩减,但是绝大多数React用户只使用一个渲染器,比如React DOM)
React模块只是让我们能够方便的使用React的特性,但是它并不负责实现。这些渲染器模块(包)(比如,react-native, react-dom等等)提供了React特性的真正实现以及平台相关的逻辑。有一些代码是共享的(reconciler)但是这只是在众多独立渲染器中一个实现。
现在我们知道为什么React和 React-dom 需要同时更新新的特性。举个例子,当React 16.3 增加新的 Context Api的时候,React.createContext()会暴露出来。
但是 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;
}
当你在代码中使用
所以如果你更新 react 版本到 16.3 及以上但是没有升级 react-dom的版本,你会使用无法感知 Provier 和 Consumer 类型的渲染器。这也是为什么老版本的 react-dom 会报错:这些类型是非法的。
相同的警告也应用于 React Native。然而,不像 React DOM, React新版本的发布不会马上 “强制”React Native发布新版本。他们有一个独立的发布周期表。每隔几周更新的渲染器代码会分开一次性同步到React Native的代码库。这也是为什么React Native新特性的更新出现和React DOM的周期是不同。