FENews

setState如何知道该做什么?

2019年02月26日    译者:$Jason X 

当你在组件里调用了 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 根据 state 改变后的 { 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.Component 继承的组件运行在 React Native 环境中这种情况下。在这种情况下调用 this.setState() 跟我们上面的做的一样,但是 React Native 是运行在 Android 和 IOS 原生环境中的,而不是 DOM 环境。

你可能对 React Test 或者 Shadllow Render 熟悉一些。这两种测试策略都允许你渲染组件,也可以在它们内部调用 this.setState() 。 但是它们都不工作在 DOM 环境中。

如果你用过例如 React ART 这样的渲染器(renderer),你可能也知道可以在一个页面里用多个渲染器(renderer)。(比如说,ART 组件可以运行在 React DOM 树中)基于这种情况,那么也就意味着全局的标识或变量这种方案是不可能的。

因此 React Component 是一种委托。委托特定的平台去处理 state 的状态更新。在我们了解它是怎么做的之前,首先让我们来深入探讨下 React 是如果做分包的,和为什么它要这么做。


有一种常见的误解认为 React 渲染引擎包含在 react 这个包中。其实不是这样的。

实际上,自从 React 0.14分包后,react 包有意地只暴露了定义组件的API。 React渲染引擎的大部分实现都在 renderer 中。

react-domreact-dom/serverreact-nativereact-test-rendererreact-art 这是一些 renderer 的例子(你可以编写你的 renderer

这就是为什么 react 包那么重要而不用关心你的目标平台是什么。它的所有导出,例如 React.ComponentReact.createElementReact.Children还有Hooks,都是独立于目标平台的。不管你是运行在 React DOM、React DOM Server、或者 React Native 中,你的组件都将以相同的方式导入,使用。

相比之下,渲染器(renderer)包公开了特定于平台的 API,比如说 ReactDOM.render() 它允许一个 React 的结构挂载到一个 DOM 节点中。每个渲染器(renderer)都提供了类似上面的 API。理想情况下,大部分的组件不需要从渲染器(renderer)中导入任何东西。这使得它们更加轻量。

大部分人都认为 React 渲染引擎的实现在每个特定的渲染器(renderer)中。 每个渲染器(renderer)中都包含有相同的代码 — 我们把它叫做协调器(reconciler)。在构建阶段我们为了更好的性能,将协调器(reconciler)和渲染器(renderer)代码打包到一个高度优化的 bundle 包里。(复制代码通常情况下对包的大小的控制不太友好,但是大多数 React 的开发者同时只需要用一种渲染器(renderer),比如说 react-dom。)

这里要说明的是,react 包只是提供给了我们用 React 功能的方法,其实不知道任何关于它们怎么实现的细节。渲染器(renderer)包(react-domreact-native,等等)提供了实现 React 功能的具体实现还有一些特定平台的逻辑。其中的一些代码是共享的(协调器(reconciler)),那些都是特有平台的具体实现。


那么我们现在知道为什么 react react-dom 包需要都更新到最新的版本了。我们举个栗子,当 React16.3 添加了 Context API, React 包提供了 React.createContext() 这个方法。

但是 React.createContext() 事实上就没有实现 Context API 这个新功能。那么 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,那么你就会用了一个没有实现 ProviderConsumer 类型的 渲染器(renderer) 这就是老版本的 react-dom 会报 fail saying these types are invalid 这个错误的原因。

在 React Native 中也会有上面同样的警告。但是不同于 React DOM,React 的发布版本不会立刻就对应到一个 React Native 的版本。React Native 有独立的发布计划表。更新的渲染器(renderer)代码将会在几周后单独同步更新到 React Native 的项目中。这就是为什么一些新功能在 React Native 和 React DOM 可用的时间点不同的原因。


好的,我们已经知道了 react 包中没有包含我们感兴趣的内容,因为这些实现都在像 react-domreact-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 ComponentsetState 的实现 它们做的就是代理到渲染器(renderer)中,让它去处理。

// A bit simplified
setState(partialState, callback) {
  // Use the `updater` field to talk back to the renderer!
  this.updater.enqueueSetState(this, partialState, callback);
}

React DOM Server 可能会忽略 State 的更新,然后警告你 而 React DOM 和 React Native 会让协调器(reconciler)处理

那么这就是为什么 this.setState() 可以更新 DOM 即使它定义在 React 包中。它去读取 React DOM 设置的 this.update 字段,让 React DOM 安排并处理更新。


现在我们知道在在类中 setState() 是怎么处理的,那么问题来了,Hooks 是怎么处理的呢?

当开发者第一次看到 Hook API提案,他们总是在想: useState 是怎么知道该干什么的?我们假设它比基于 React.ComponentsetState() 更加神奇。

但是正如我们今天所看到的,基于类的 setState() 实现是一种错觉。除了将调用转发给当前渲染器(renderer)之外,它不会执行任何操作。useState Hook 做了同样的事情

Hooks 使用 “dispatcher” 对象,而不是 updater 字段。当你调用 React.useState()React.useEffect(),或者其他内置 Hooks 的时候,它们通通转发给当前的 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副本 时,这可能不 work。这总是导致了一些模糊的错误,因此 Hooks 会强迫你解决包重复问题。

你可以在技术上覆盖 dispatcher 已获取高级工具功能,但是我们不鼓励这么做(其实 __currentDispatcher 属性不是真实的属性名称, 你可以在 React 项目中找到真实的名字)。举个栗子,React DevTools 将使用 特殊的提案 - built dispatcher 通过捕获 JavaScript 堆栈树来观察 Hooks。

这也意味着 Hooks 本身并不依赖于 React。如果将来有更多的库,想要重用相同的原始 Hooks, 理论上 dispatcher 可以单独封装到一个包里并导出一个。在实际开发中,我们要避免过早的抽象直到我们真正需要抽象的时候。

updater 字段 和 __currentDispatcher 对象都是通用编程原则的形式叫做 依赖注入。在这两种情况下,渲染器(renderer)将诸如 setState 之类的功能的实现“注入”到通用的 React包 中,以使组件更具声明性。

当你用 React 的时候根本不用去想它是怎么工作的。我们希望 React 用户花更多时间考虑他们的应用程序代码,而不是考虑依赖注入这样的抽象概念。但是如果你想知道 this.setState() 或者 useState() 这些东西的工作原理,我希望这篇文章能到帮到你。

原文地址:https://overreacted.io/how-does-setstate-know-what-to-do/


FENews 是由一群热爱技术的前端小伙伴自发组成的团队。团队会定期创作和翻译前端相关的技术文章,同时我们也欢迎外部投稿或加入我们的核心编辑团队。如果您对我们感兴趣,请关注我们的公众号:

FENews