热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

setState是如何知道该怎么做的?

要有心里准备,这篇文章抽象又拗口,希望有人可以将它视觉化!当你在组件里调用很明显,React会随着新

要有心里准备,这篇文章抽象又拗口,希望有人可以将它视觉化!

当你在组件里调用 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 

Thanks

; } return ( ); } } ReactDOM.render(

很明显,React会随着新的 { clicked: true} 状态重渲染组件(component),更新DOM,匹配返回

Thanks

元素(element)。

似乎很简单。不过问题来了,是 React 干的还是 React DOM 干的?

更新DOM听起来像 React DOM 负责的,但我们调用 this.setState() ,和 React DOM 似乎没有关联, React.Component 这个基类是在React中声明的。

那么 React.Component 中的 setState() 是如何更新DOM的?

免责声明:与多数 其他 文章 一样,这篇文章,对React实际使用来说不是必须的,它适合喜欢追寻万物原理的朋友们,谨慎选择 !

我们可能认为 React.Component 包含了更新DOM的逻辑。

但是如果是这样的话, this.setState() 如何在其他环境奏效?例如,React Native 的组件也扩展了 React.Component ,它们就像前面那样调用 this.setState() ,且 React Native 使用在Android和iOS原生视图而不是DOM。

你可能也会对 React的 Test Renderer 或 Shallow Renderer 有些印象,这两种测试方案都可以渲染普通组件并在其中调用 this.setState() ,但它们和DOM都没关系。

如果你用过像 React ART 这样的渲染器(renderer),你可能也知道页面有可能使用多个渲染器(例如,ART组件运行于React DOM树中),这使得全局标志或变量不再可靠。

所以,针对不同平台代码, React.Component 以某种委托方式处理state更新 。在我们弄清楚怎么回事前,先深入探讨下如何及为什么要分离包(packages)。

有一种常见的误解,即React的“引擎”在 react 依赖包中,这不是真的。

实际上,自从React 0.14拆分依赖包以来, react 依赖包特意地只暴露 定义 组件(components)的APIs,React绝大多数 实现 都放在 “渲染器”,

react-domreact-dom/serverreact-nativereact-test-rendererreact-art 都是渲染器样例(你可以 搭建自己的 )。

这也是为什么 react 依赖包不管面向哪个平台都可行,它所有的导出,例如 React.ComponentReact.createElementReact.Children 和最近的Hooks,都独立于目标平台,无论你运行 React DOM、React DOM Server或者React Native,你都可以用同一种方式导入使用组件。

相比之下,渲染器依赖包暴露特定平台的APIs,如 ReactDOM.render() ,可以将React组件插入DOM节点中。每个渲染器都会提供一个类似的API,理想情况下,大多数 组件 不需要从渲染器导入任何内容,这使它们更灵活。

大多数人认为React的“引擎”在每个渲染器中。不过许多渲染器确实包含了同一份副本代码 —— 我们称为 "reconciler" 。有个构建步骤将 reconciler 代码与渲染器代码融合成一份高度优化过的代码,以获得更好的性能。(通常不利于依赖包大小,但绝大多数用户一次只需要一个渲染器,例如 react-dom )

这里要说的是, react 依赖包只让你知道React有哪些功能,但不知道功能是如何实现的。渲染器依赖包( react-domreact-native 等)提供了React功能的实现和平台特性的逻辑。其中一些代码是共享的("reconciler"),但更多的是各个渲染器的具体实现。

现在我们知道为什么有功能时, reactreact-dom 依赖包需要同时更新了,比如说,在React 16.3添加 Context API 时,React依赖包会暴露 React.createContext()

React.createContext() 实际上并没有 实现 context功能,React DOM 与 React DOM Server 的实现是不同的。例如, createContext 返回一些 plain objects:

// 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;
}

当你在代码里使用 或者 时, 渲染器 决定如何处理它们。React DOM可能以一种方式跟踪context,而React DOM Server可能会采用另一种方式。

如果你更新 react 到16.3+而没更新 react-dom ,你将使用的渲染器便不知道什么是 ProviderConsumer 这也是旧的 react-dom 会引发类型无效错误的原因

React Native同样有这警告。不过不同于 React DOM,一次React更新发布不会“迫使”React Native也立即发布新版本,它有自己一套发行时间表。更新的渲染器代码将 单独同步 到React Native代码库中。所以React Native和React DOM同一个功能,可以用上的时间是不同的。

好了,我们现在知道 react 依赖包不包含任何有趣的内容,因为具体实现放到 react-domreact-native 等渲染器中了。但是这没能解决我们的问题, React.Component 中的 setState() 是如何与对应的渲染器“交流的”。

答案是每个渲染器在创建的class上设置一个特殊字段。这个字段叫做 updater 。这不是由你设置的,而是React DOM、React DOM Server、React Native在你实例class后给你加上的:

// 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.Component 中的 setState 实现 ,它所做的就是将任务全部委托给实例此组件的渲染器:

// 简化后的代码
setState(partialState, callback) {
  // 用`updater` 反馈给渲染器
  this.updater.enqueueSetState(this, partialState, callback);
}

React DOM Server 也许打算 忽略state更新并警告你,而React DOM和React Native会用复制来的"reconciler"去 处理它 。

这也是为什么即使 this.setState() 定义在React依赖包中,依然可以更新DOM。它会获取由React DOM设置的 this.updater ,并让React DOM调度和处理更新。

我们现在知道class了,那Hooks是怎么做的?

当大家第一次看到Hooks API,很可能会想: useState 怎么“知道该怎么做”?猜想是它的 this.setState() 比基于 React.Component 的更“神奇”。

但正如我们今天看到的,基于class的 setState() 实现一直是一种错觉,除了调用指向当前的渲染器之外,它不参与任何操作。 useState Hook 也同样如此 。

Hooks使用 dispatcher 对象而不是 updater 字段 。在你调用 React.useState()React.useEffect() 、或者其他内置Hook时,这些都会转发给当前的dispatcher。

// In React (简化)
const React = {
  // 真正的属性隐藏得有点深,你可以尝试去找找看!
  __currentDispatcher: null,

  useState(initialState) {
    return React.__currentDispatcher.useState(initialState);
  },

  useEffect(initialState) {
    return React.__currentDispatcher.useEffect(initialState);
  },
  // ...
};

而每种渲染器在组件渲染之前会设置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 这样的渲染器需要获取同一个 react 依赖包的原因,否则,你的组件不会“看到”这个dispatcher!如果在同一棵组件树中存在 多个React副本 ,就有可能发生问题。不过这样容易出现隐蔽bug,所以Hooks会强迫你在发生前就解决依赖包重复问题。

虽然我们不鼓励这样做,但为了更适用于某些情景,你可以在技术上自行覆盖dispatcher( __currentDispatcher 是我编造的,不过你可以在代码库中找到真实的名称),例如,React DevTools会用 一个专门定制的dispatcher 通过捕获Javascript堆栈轨迹来描绘反馈Hooks树。 不要在家重复这样做了

这也意味着Hooks本身并不依赖于React。如果将来有更多的类库想复用React里的Hooks理念,理论上dispatcher可以挪过去用并且作为一个更少“可怕”名称的一流API展现出来。在开发过程中,我们应该避免过早抽象概念,直到我们不得不这么做了。

updater 字段和 __currentDispatcher 对象都形成于一个叫 依赖注入 的通用编程原理。这两种情况里,渲染器将诸如 setState 之类的功能实现“注入”到通用的React依赖包中,组件因此以声明为主。

在使用React时,你不需要思考这些是怎么跑起来的。我们希望React开发者花更多的时间在应用程序代码上,而不是像依赖注入这些抽象概念上。但如果你想知道 this.setState() 或者 useState 是如何知道怎么做的,我希望这会有所帮助。

翻译原文 How Does setState Know What to Do? (2018-12-09)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 我们


推荐阅读
author-avatar
萍水相逢--莹
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有