[译]react的setState如何知道该做什么 --Dan Abramov

1,061 阅读8分钟

原文: How Does setState Know What to Do?

原译文: react的setState如何知道他要做什么

译:可能看到标题的时候会想,怎么去做还不是看代码吗?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-domreact-dom / serverreact-nativereact-test-rendererreact-art是在renderers中的一些例子(你也可以建立属于你自己的)。

所以,无论在什么平台,react包都可以正常工作。他对外暴露的所有的内容,例如: React.ComponentReact.createElementReact.Children的作用和Hook,都独立于目标平台。无论运行React DOMReact 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-domreact-native等)提供了React功能和特定于平台的逻辑的实现。其中一些代码是共享的(“reconciler”),但这是各个渲染器的实现细节。


现在我们知道了为什么每次有新的功能都会同时更新reactreact-dom包。例如,当React 16.3添加了Context API时,React.createContext()在React包上对外暴露。

但是React.createContext实际上并没有实现上下文功能。例如,React DOMReact 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将是一个无法解析ProviderConsumer类型的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 DOMReact DOM ServerReact 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做了同样的事情

相对于setStateupdater字段而言,Hooks使用dispatcher对象。 当调用React.useStateReact.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 DOMReact 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.setStateuseState如何知道该怎么做,我希望本文会对你有所帮助。