当你在组件中调用`setState`的时候,你认为发生了些什么?
~~~jsx
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}`重新渲染组件,同时更新DOM以匹配返回的`<h1>Thanks</ h1>`元素啊。
看起来很直白。但是等等,是*React*做了这些吗?还是*React DOM*?
更新DOM听起来像是React DOM的职责所在。但是我们调用的是`this.setState()`,而没有调用任何来自React DOM的东西。 而且我们组件的父类`React.Component`也是在React本身定义的。
所以存在于`React.Component`内部的`setState()`是如何更新DOM的呢?
**免责声明: 就像本博客里[绝大多数](https://overreacted.io/zh-hans/why-do-react-elements-have-typeof-property/)[其他的](https://overreacted.io/zh-hans/how-does-react-tell-a-class-from-a-function/)[帖子](https://overreacted.io/zh-hans/why-do-we-write-super-props/)一样, 其实你不*需要*知道其中的任何知识,就可以有效地使用React。本文面向的是那些想要了解React背后原理的人。而这完全是可选的!**
我们或许会认为:`React.Component`类包含了DOM更新的逻辑。
但是如果是这样的话,`this.setState()`又如何能在其他环境下使用呢?举个例子,React Native app中的组件也是继承自`React.Component`。他们依然可以像我们在上面做的那样调用`this.setState()`,而且React Native渲染的是安卓和iOS原生的界面而不是DOM。
你或许对React Test Renderer 或是 Shallow Renderer很熟悉。这些测试策略能让你正常渲染组件,也可以在组件内部调用`this.setState()`。但是这两个渲染器并不与DOM相关。
如果你曾使用过一些渲染器像[React ART](https://github.com/facebook/react/tree/master/packages/react-art),你也许也知道在一个页面中我们是可以使用多个渲染器的。(举个例子,ART 组件在React DOM树的内部起作用。)这使得全局标志或变量无法维持。
因此,**`React.Component`以某种未知的方式将处理状态(state)更新的任务委托给了特定平台的代码。**在我们理解这些是如何发生的之前,让我们深挖一下包(packages)是如何分离的以及为什么这样分离。
> 有一个很常见 的误解就是React“引擎” 是存在于`react`包里面的。 然而事实并非如此。
>实际上从[React 0.14](https://reactjs.org/blog/2015/07/03/react-v0.14-beta-1.html#two-packages)我们将代码拆分成多个包以来,`react`包故意只暴露一些定义组件的API。绝大多数React的*实现*都存在于“渲染器(renderers)”中。
`react-dom`、`react-dom/server`、`react-native`、`react-test-renderer`、`react-art`都是常见的渲染器(当然你也可以[创建属于你的渲染器](https://github.com/facebook/react/blob/master/packages/react-reconciler/README.md#practical-examples))。
这就是为什么不管你的目标平台是什么,`react`包都是可用的。从`react`包中导出的一切,比如`React.Component`、`React.createElement`、`React.Children`和(最终的)[Hooks](https://reactjs.org/docs/hooks-intro.html),都是独立于目标平台的。无论你是运行React DOM,还是 React DOM Server,或是 React Native,你的组件都可以使用同样的方式导入和使用。
相比之下,渲染器包暴露的都是特定平台的API,比如说:`ReactDOM.render()`,可以让你将React层次结构(hierarchy)挂载进一个DOM节点。每一种渲染器都提供了类似的API。理想状况下,绝大多数*组件*都不应该从渲染器中导入任何东西。只有这样,组件才会更加灵活。
**和大多数人现在想的一样,React “引擎”就是存在于各个渲染器的内部。**很多渲染器包含一份同样代码的复制 —— 我们称为[“协调器”(“reconciler”)](https://github.com/facebook/react/tree/master/packages/react-reconciler)。[构建步骤(build step)](https://reactjs.org/blog/2017/12/15/improving-the-repository-infrastructure.html#migrating-to-google-closure-compiler)将协调器代码和渲染器代码平滑地整合成一个高度优化的捆绑包(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()`只返回了一些普通对象:
~~~jsx
// 简化版代码
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`会[报错说这些类型是无效的](https://stackoverflow.com/a/49677020/458193)。
同样的警告也会出现在React Native中。然而不同于React DOM的是, 一个React新版本的发布并不立即“强制”发布新的 React Native 版本。他们具有独立的发布日程。 每隔几周,更新后的渲染器代码就会[单独同步到](https://github.com/facebook/react-native/commits/master/Libraries/Renderer/oss)React Native仓库。这就是相比 React DOM,React Native 特性可用时间不同的原因。
* * *
好吧,所以现在我们知道了`react`包并不包含任何有趣的东西,除此之外,具体的实现也是存在于`react-dom`,`react-native`之类的渲染器中。但是这并没有回答我们的问题。`React.Component`中的`setState()`如何与正确的渲染器“对话”?
> 答案是:每个渲染器都在已创建的类上设置了一个特殊的字段。**这个字段叫做`updater`。这并不是*你*要设置的的东西——而是,React DOM、React DOM Server 或 React Native在创建完你的类的实例之后会立即设置的东西:
~~~jsx
// 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`的实现](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react/src/ReactBaseClasses.js#L58-L67),`setState`所做的一切就是**委托渲染器创建这个组件的实例**:
![](https://img.kancloud.cn/c3/65/c36596a9ad922d74a5a567c98611faf4_1488x420.png)
~~~jsx
// 适当简化的代码
setState(partialState, callback) {
// 使用`updater`字段回应渲染器!
this.updater.enqueueSetState(this, partialState, callback);
}
~~~
React DOM Server[也许想](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-dom/src/server/ReactPartialRenderer.js#L442-L448)忽略一个状态更新并且警告你,而React DOM 与 React Native却想要让他们协调器(reconciler)的副本[处理它](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-reconciler/src/ReactFiberClassComponent.js#L190-L207)。
> 这就是this.setState()`尽管定义在React包中,却能够更新DOM的原因。它读取由React DOM设置的`this.updater`,让React DOM安排并处理更新。
* * *
现在关于类的部分我们已经知道了,那关于Hooks的呢?
当人们第一次看见[Hooks proposal API](https://reactjs.org/docs/hooks-intro.html),他们可能经常会想:`useState是`怎么 “知道要做什么”的?然后假设它比那些包含`this.setState()`的`React.Component`类更“神奇”。
但是正如我们今天所看到的,基类中`setState()`的执行一直以来都是一种错觉。它除了将调用转发给当前的渲染器外,什么也没做。`useState`Hook[也是做了同样的事情](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react/src/ReactHooks.js#L55-L56)。
> **Hooks使用了一个“dispatcher”对象,代替了`updater`字段。**当你调用`React.useState()`、`React.useEffect()`、 或者其他内置的Hook时,这些调用被转发给了当前的dispatcher。
~~~jsx
// React内部(适当简化)
const React = {
// 真实属性隐藏的比较深,看你能不能找到它!
__currentDispatcher: null,
useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},
useEffect(initialState) {
return React.__currentDispatcher.useEffect(initialState);
},
// ...
};
~~~
各个渲染器会在渲染你的组件之前设置dispatcher:
~~~jsx
// React DOM 内部
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;let result;
try {
result = YourComponent(props);
} finally {
// 恢复原状 React.__currentDispatcher = prevDispatcher;}
~~~
举个例子, React DOM Server的实现是在[这里](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-dom/src/server/ReactPartialRendererHooks.js#L340-L354),还有就是React DOM 和 React Native共享的协调器的实现在[这里](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-reconciler/src/ReactFiberHooks.js)。
这就是为什么像`react-dom`这样的渲染器需要访问那个你调用Hooks的`react`包。否则你的组件将不会“看见”dispatcher!如果在一个组件树中存在[React的多个副本](https://github.com/facebook/react/issues/13991),也许并不会这样。但是,这总是导致了一些模糊的错误,因此Hooks会强迫你在出现问题之前解决包的重复问题。
在高级工具用例中,你可以在技术上覆盖dispatcher,尽管我们不鼓励这种操作。(对于`__currentDispatcher`这个名字我撒谎了,但是你可以在React仓库中找到真实的名字。)比如说, React DevTools将会使用[一个专门定制的dispatcher](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-debug-tools/src/ReactDebugHooks.js#L203-L214)通过捕获JavaScript堆栈跟踪来观察Hooks树。*请勿模仿。*
这也意味着Hooks本质上并没有与React绑定在一起。如果未来有更多的库想要重用同样的原生的Hooks, 理论上来说dispatcher可以移动到一个分离的包中,然后暴露成一个一等(first-class)的API,然后给它起一个不那么“吓人”的名字。但是在实践中,我们会尽量避免过早抽象,直到需要它为止。
`updater`字段和`__currentDispatcher`对象都是称为*依赖注入*的通用编程原则的形式。在这两种情况下,渲染器将诸如`setState`之类的功能的实现“注入”到通用的React包中,以使组件更具声明性。
使用React时,你无需考虑这其中的原理。我们希望React用户花更多时间考虑他们的应用程序代码,而不是像依赖注入这样的抽象概念。但是如果你想知道`this.setState()`或`useState()`是如何知道该做什么的,我希望这篇文章会有所帮助。
## 摘自
[ setState如何知道该做什么?](https://overreacted.io/zh-hans/how-does-setstate-know-what-to-do/)
- 文档说明
- 大厂面试题
- HTML
- 001.如何遍历一个dom树
- 002.为什么操作DOM会很慢
- 003.浏览器渲染HTML的步骤
- 004.DOM和JavaScript的关系
- JS
- 001.数组扁平化并去重排序
- 002.高阶函数
- 003.sort() 对数组进行排序
- 004.call 、 apply 和bind的区别
- 006.0.1+0.2为什么等于0.30000000000000004
- 011.var、let、const 的区别及实现原理?
- 010.new操作符都做了什么
- 009.a.b.c.d 和 a['b']['c']['d'],哪个性能更高?
- 016.什么是防抖和节流?有什么区别?如何实现?
- 017.['1', '2', '3'].map(parseInt) what & why ?
- 018.为什么 for 循环嵌套顺序会影响性能?
- 019.介绍模块化发展历程
- 020.push输出问题
- 021.判断数组的三个方法
- 022.全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?
- 023.输出以下代码的执行结果并解释为什么
- 024.ES6 代码转成 ES5 代码的实现思路是什么
- 025.为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。
- 026.数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少
- 027.变量类型
- 028.原型和原型链
- 029.作用域和闭包
- 030. 异步
- 031.ES6/7 新标准的考查
- 024.事件冒泡/事件代理
- 025.手写 XMLHttpRequest 不借助任何库
- 026.什么是深拷贝?
- 0027.克隆数组的方法
- 0028.ES6之展开运算符(...)
- 0029.arguments
- 0030. requestAnimationFrame
- 0031.递归爆栈问题与解决
- 021.简单改造下面的代码,使之分别打印 10 和 20
- 032.箭头函数与普通函数
- 033.去除掉html标签字符串里的所有属性
- 034.查找公共父节点
- 035.Promise
- 0036.JSON.stringify ()
- CSS
- 001. BFC
- 002.介绍下 BFC、IFC、GFC 和 FFC
- 003.分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景
- 004.怎么让一个 div 水平垂直居中
- 005.重排重绘
- 006.inline/block/inline-block的区别
- 007.选择器的权重和优先级
- 008.盒模型
- 009.清除浮动
- 010.flex
- 011.nth-child和nth-of-type的区别
- 0012.overflow
- 0013.CSS3中translate、transform和translation的区别和联系
- 0014.flex
- 0015.px、em、rem
- 0016.width:100%
- 网络
- 001.讲解下HTTPS的工作原理
- 002.介绍下 HTTPS 中间人攻击
- 003.谈谈你对TCP三次握手和四次挥手的理解
- 004.A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态
- 005.简单讲解一下http2的多路复用
- 006. 介绍下 http1.0、1.1、2.0 协议的区别?
- 007.永久性重定向(301)和临时性重定向(302)对 SEO 有什么影响
- 008.URL从输入到页面展示的过程
- 009.接口如何防刷
- 010.http状态码?
- 0111.跨域/如何解决?
- 012.cookie 和 localStorage 有何区别?
- 013.Fetch API
- 014.跨域Ajax请求时是否带Cookie的设置
- 0015.协商缓存和强缓存
- 性能优化
- 001.前后端分离的项目如何seo
- 002.性能优化的方法
- 003.防抖和节流
- React
- 001.React 中 setState 什么时候是同步的,什么时候是异步的?
- 002.Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。
- 003.Hooks 的特别之处
- 004.元素和组件有什么区别?
- 005.什么是 Pure Components?
- 006.HTML 和 React 事件处理有什么区别?
- 007.如何将参数传递给事件处理程序或回调函数?
- 008.如何创建 refs?
- 009.什么是 forward refs?
- 010.什么是 Virtual DOM?
- 011.什么是受控组件、非受控组件?
- 012.什么是 Fragments ?
- 013.为什么React元素有一个$$typeof属性?
- 014.如何在 React 中创建组件?
- 015.React 如何区分 Class 和 Function?
- 016.React 的状态是什么?
- 017.React 中的 props 是什么?
- 018.状态和属性有什么区别?
- 019.如何在 JSX 回调中绑定方法或事件处理程序?
- 020.什么是 "key" 属性,在元素数组中使用它们有什么好处?
- 021.为什么顺序调用对 React Hooks 很重要?
- 022.setState如何知道该做什么?
- 023.hook规则?
- 024.Hooks 与 Class 中调用 setState 有不同的表现差异么?
- 025.useEffect
- 026.fiber的作用
- 027.context的作用?
- 028.setState何时同步何时异步?
- 029.react性能优化
- 030.fiber
- 031.React SSR
- 异步
- 001.介绍下promise
- 002.Async/Await 如何通过同步的方式实现异步
- 003.setTimeout、Promise、Async/Await 的区别
- 004.JS 异步解决方案的发展历程以及优缺点
- 005.Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
- 006.模拟实现一个 Promise.finally
- 012.简单手写实现promise
- 015.用Promise对象实现的 Ajax
- 007.简单实现async/await中的async函数
- 008.设计并实现 Promise.race()
- 009.Async/await
- 010.珠峰培训promise
- git
- 001.提交但没有push
- 002.gitignore没有作用?
- Node
- 001.用nodejs,将base64转化成png文件
- Koa
- 001.koa和express的区别
- 数据库
- redux
- 001.redux 为什么要把 reducer 设计成纯函数
- 002.在 React 中如何使用 Redux 的 connect() ?
- 003.mapStateToProps() 和 mapDispatchToProps() 之间有什么区别?
- 004.为什么 Redux 状态函数称为 reducers ?
- 005.如何在 Redux 中发起 AJAX 请求?
- 006.访问 Redux Store 的正确方法是什么?
- 007.React Redux 中展示组件和容器组件之间的区别是什么?
- 008.Redux 中常量的用途是什么?
- 009.什么是 redux-saga?
- 设计模式
- 公司题目
- 001.饿了么
- 001.div垂直水平居中(flex、绝对定位)
- 002.React子父组件之间如何传值
- 003.Emit事件怎么发,需要引入什么
- 004.介绍下React高阶组件,和普通组件有什么区别
- 005.一个对象数组,每个子对象包含一个id和name,React如何渲染出全部的name
- 006.在哪个生命周期里写
- 007.其中有几个name不存在,通过异步接口获取,如何做
- 008.渲染的时候key给什么值,可以使用index吗,用id好还是index好
- 009.webpack如何配sass,需要配哪些loader
- 010.配css需要哪些loader
- 011.如何配置把js、css、html单独打包成一个文件
- 012.监听input的哪个事件,在什么时候触发
- 013.两个元素块,一左一右,中间相距10像素
- 014.上下固定,中间滚动布局如何实现
- 016.取数组的最大值(ES5、ES6)
- 017.apply和call的区别
- 018.ES5和ES6有什么区别
- 019.some、every、find、filter、map、forEach有什么区别
- 020.上述数组随机取数,每次返回的值都不一样
- 021.如何找0-5的随机数,95-99呢
- 022.页面上有1万个button如何绑定事件
- 023.如何判断是button
- 024.页面上生成一万个button,并且绑定事件,如何做(JS原生操作DOM)
- 025.循环绑定时的index是多少,为什么,怎么解决
- 026.页面上有一个input,还有一个p标签,改变input后p标签就跟着变化,如何处理
- 浏览器相关
- 001.性能优化
- 002.web安全
- 003.获取浏览器大小
- 004.从输入 URL 到页面加载完成的过程中都发生了什么事情?
- 后端
- 001.分布式
- zuku
- 字节
- webpack
- webpack的打包原理是什么
- Webpack-- 常见面试题
- webscoket