How Does setState know what to d
how Does setState know What to do? - Dan
当你在组件中调用 setState
时,你认为发生了什么?
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Button extends Component {
constructor(props) {
super(props);
this.state = { clicked: fasle };
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>
)
}
}
当然,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.Component
中包含更新DOM的逻辑。
但如果真是这样的话, this.setState()
是怎么在其它环境中运行的呢?例如,在React Native中组件同样也继承了 React.Component
。RN里面也会调用 this.setState()
,但是RN运行在Android和iOS原生视图中,而不是DOM中。
你可能也属性React Test Renderer 或者 shallow Renderer。这2个测试策略使你可以渲染普通组件并且在里面调用this.setState()
。但是它们都和DOM无关。
如果你使用像 React ART 这样的渲染器,你可能知道在页面上可能使用超过一个renderer。(例如,ART组件在React DOM 树中使用),这使全局flag或者变量变得无法维持。
因此 React.Component
以某种方式委托处理状态更新到特定平台的代码,在我们了解这是如何发生之前,让我们深入了解包的分离方式和原因。
<hr />
有一个常见的误解就是,React 的 engine
存放在 react
包中,这是不正确的。
实际上,从 react 0.14
以后,react 有意只暴露定义组件的接口,React 的大多数实现都依托于 renderers
react-dom
, react-dom/server
, react-native
, react-test-renderer
, react-art
就是 renderers 的示例(你可以构建自己的渲染器)。
这也是为什么 react
对不同的平台都有用的原因。它导出的,例如 React.Component
, React.createElement
, React.Children
工具和(最终的)Hooks, 是和目标平台独立开来的。是否你运行React DOM,React DOM Server 或者React Native,你的组件的引入和使用方式都是一样的。
相比之下,renderer则暴露出平台相关的接口,比如 ReactDOM.render()
可以使你将React 层级渲染到DOM节点中。每个renderer都提供了类似的接口。理想情况下,大多数组件不应该从renderer中引入认定和东西,这样可以使组件变得protable(即像移动硬盘一样,即插即用)。
大多数遐想的是 React 'engine' 存在于每一个 renderer中。大多数renderers都包含某些相同代码的拷贝 - 我们称之为 reconciler。
一个 设置步骤 将 reconciler代码和renderer code 平滑的集成到一个高度优化的bundle中,提升性能(拷贝代码对打包的尺寸不太友好,但是大多数React users通常一次只需要一种renderer,比如 react-dom)。
上面讲的核心意思是,react
包只让你使用react功能,而对于怎么实现react包是不管的。renderer包(比如 react-dom,react-native等)提供了React Features 的实现 和平台指定的逻辑。某些代码是共享的(reconciler),但这是各个渲染器的实现细节。
<hr />
现在我们应该理解了为什么对新功能我们需要同时升级react和react-dom了。例如,对 React 16.3
添加了 Context
接口, 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.Consumer>
,是 renderer 决定如何去处理它们的。React DOM 可能以某种方式追踪context values,而React DOM Server又以另一种方式处理。
因此如果你更新 react
到 16.3
, 但是没有更新 react-dom,你将使用一个无法意识到 Provider
和 Consumer
类型是特别类型的renderer。这也是为什么老版本 react-dom
可能说这些类型无效的原因。
相同的问题对React Native同样是一样的,但是和react dom不同的是,react的发布并不会立即强制一个react native 的发布。它们拥有一个独立的发布计划。更新的渲染器代码在几周内会单独的同步到React Native的仓库中。这就是为什么React Native的功能获取计划和react dom不一样的原因。
<hr />
好了,我们已经知道了 react 包中不包含任何又去的东西,所有实现都在渲染器(比如react-native, react-dom)中。但是这并没有回答我们的问题:在 React.Component
中的 setState()
是如何和正确的渲染器之间沟通的。
答案就是,每个渲染器在创建的class中设置了一个特别的字段, 这个字段就叫 updater
。这不是你要设置的,而是React DOM, React Native等在创建你类的实例时需要做的。
// 在React DOM内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater; // 设置 updater
// 在React DOM Server 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater; // 设置 updater
// 在React Native 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater; // 设置 updater
参看 setState 在 React.Component 中的实现,它所做的是将任务委托给创建组件实例的渲染器
// 简化版
setState(partialState, callback) {
// 使用 'updater' 属性 talk back to 渲染器
this.updater.enqueueSetState(this, partialState, callback);
}
React DOM Server 可能想忽略状态的更新和警告你,然而 react dom和react native 则会让 它们拷贝的reconciler去处理这些问题.
这是为什么 this.setState()
定义在React包中也能更新DOM的原因, 它读取React DOM 设置的this.updater
,然后让React DOM计划和处理更新。
<hr />
我们现在知道classes了,那Hooks呢?
当人们第一次看到 Hooks Proposal API时,总会疑惑, 为什么 useState
知道要做什么呢? 可能会假设这个比 React.Component这个基类中的this.setState()
更加的magical(神奇)。
但是由上面所看到的,基类中 setState()
的实现一直是一个假象,它除了将调用转发给当前的渲染器以外,其实什么也没有做。 useState
也 做了一模一样的事情。
不同于使用 updater
对象, useState
使用 dispatcher
对象。当你调用 React.useState() | React.useEffect()
或者其它内置的钩子时,这些调用都被转发给当前的dispatcher了。
// 在React 中 (简化版)
const React = {
// 真是属性被隐藏的更深一些,是否你可以找到它
__currentDispatcher: null,
useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},
useEffect(initialState) {
return React.__currentDispather.useEffect(initialState);
},
// ...
}
在渲染你的组件前,每个渲染器都会设置这个 dispatcher:
// 在 React DOM中
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher; // 设置dispatcher
let result;
try {
result = YourComponent();
} finally {
// Restore it back 还原
React.__currentDispatcher= prevDispatcher;
}
例如,React DOM Server的实现 在这里, React DOM 和 React Native 之间共享的reconciler实现 在这。
这就是为什么一个渲染器,比如react-dom需要访问你调用Hooks的同一个react包的原因,否则,你的组件可能看不到 dispatcher
。当你在相同组件树有 多个React拷贝 时,可能无法正常运行的原因。然而,这总是导致一个模糊的错误,所有Hooks会强迫你在出现问题前解决包重复的问题.
你可以为了实现高级功能改写dispatcher,但是不鼓励这样做。(我对 __currentDispatcher
名字说谎了,但是你可以在React仓库中找到真正的)。例如,React DevTools 将使用 特别目的构建的dispatcher 通过捕获js堆栈跟踪内省Hooks树。
这也意味着,Hooks本身并不依赖于React。如果将来有更多的库想要复用相同的原始Hooks,理论上,dispatcher程序可以移植到一个单独的包中,并且作为第一级API以一个不太恐怖的名字暴露出去。
updater
和 __currentDispatcher** 对象是一种叫做依赖注入(dependency injection)抽象编程原则的形式。2种情况下,渲染器将 **
setState` 这样的功能实现 注入 到react这样的泛型包中,使你的组件更加的声明式。
当你使用React时不需要考虑它是如何运作的。我们希望React用户花更多的时间思考他们应用的代码而不是像依赖注入这样抽象的概念。但是如果你想知道 this.setState()
或者 useState()
做了些什么,这篇文章可能会帮助到你。
2018年12月10日16:36:13