专业编程基础技术教程

网站首页 > 基础教程 正文

业余时间总结了React的setState原理,有兴趣的了解下

ccvgpt 2024-11-24 12:33:08 基础教程 2 ℃


我唯一能确定的就是自己的无知 ——苏格拉底 (哲学之父)

业余时间总结了React的setState原理,有兴趣的了解下

目标

  • 理解setState为何知道更新
  • 理解hooks的执行者

原文链接: 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() {
  // setState
    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以匹配返回的<h1>Thanks</ h1>元素啊。

看起来很直白。但是等等,是 _React_做了这些吗 ?还是_React DOM _? **


疑惑: 我们或许会认为:React.Component类包含了DOM更新的逻辑。

但是如果是这样的话,this.setState()又如何能在其他环境下使用呢?举个例子,React Native app中的组件也是继承自React.Component。他们依然可以像我们在上面做的那样调用this.setState(),而且React Native渲染的是安卓和iOS原生的界面而不是DOM。 因此,**React.Component以某种未知的方式将处理状态(state)更新的任务委托给了特定平台的代码。**在我们理解这些是如何发生的之前,让我们深挖一下包(packages)是如何分离的以及为什么这样分离。 **


疑惑: 有一个很常见的误解就是React“引擎”是存在于react包里面的。 然而事实并非如此。

** 实际上从React 0.14我们将代码拆分成多个包以来,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所做的一切就是委托渲染器创建这个组件的实例:

// 适当简化的代码
setState(partialState, callback) {
  // 使用`updater`字段回应渲染器!
  this.updater.enqueueSetState(this, partialState, callback);
}

** 这就是this.setState()尽管定义在React包中,却能够更新DOM的原因。它读取由React DOM设置的this.updater`,让React DOM安排并处理更新。

??????小结

  1. setState缘由
  2. 存放位置以及如何通信
  3. 渲染器被指派处理state的变化。

疑惑:

当使用Hooks时,useState是怎么 “知道要做什么”的 ?

当人们第一次看见Hooks proposal 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,尽管我们不鼓励这种操作。(对于__currentDispatcher这个名字我撒谎了,但是你可以在React仓库中找到真实的名字。)比如说, React DevTools将会使用一个专门定制的dispatcher通过捕获JavaScript堆栈跟踪来观察Hooks树。请勿模仿。

这也意味着Hooks本质上并没有与React绑定在一起。如果未来有更多的库想要重用同样的原生的Hooks, 理论上来说dispatcher可以移动到一个分离的包中,然后暴露成一个一等(first-class)的API,然后给它起一个不那么“吓人”的名字。但是在实践中,我们会尽量避免过早抽象,直到需要它为止。

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

使用React时,你无需考虑这其中的原理。我们希望React用户花更多时间考虑他们的应用程序代码,而不是像依赖注入这样的抽象概念。但是如果你想知道this.setState()或useState()是如何知道该做什么的,我希望这篇文章会有所帮助。

最近发表
标签列表