当你在使用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},同时更新DOM返回
。看起来很简单,不过等一下,是React还是React DOM进行的这个操作?
更新DOM看起来是React DOM的活,但是我们执行的是this.setState(),而不是从React DOM包引入的某个函数。而且React.Component ?? 来自React内部。 Updating the DOM sounds like something React DOM be responsible for. But we’re calling this.setState(), not something from React DOM. And our React.Component base class is defined inside React itself.
所以说,为什么在React.Component中调用setState会更新DOM呢?
声明一下:跟在这个博客中的其他博文一样,你不需要知道这些也一样可以玩转React。这篇博文只是给那些想了解其中秘密的人看的。??? 只是一个可选项。
Disclaimer: Just like most other posts on this blog, you don’t actually need to know any of that to be productive with React. This post is for those who like to see what’s behind the curtain. Completely optional!
我们可能觉得React.Component类函数中含有DOM更新的逻辑。
不过如果是这样的话,**this.setState()**为什么在其他环境中也能运行呢。举个栗子,React Native中的组件同样继承自React.Component,也一样想上述那样调用 this.setState() ,可是更新的却不是DOM而是来自android和iOS的原生视图。
你可能同样听说过测试渲染或者浅层渲染,两种测试方法都可以渲染正常的组件同时在其中调用 this.setState(), 但是却都没有对DOM进行操作
如果你使用过React ART这样的渲染器的话,你可能了解到在一个页面中使用不同的渲染器是可以的。(比如说,DOM组件中包含ART组件)???? If you used renderers like React ART, you might also know that it’s possible to use more than one renderer on the page. (For example, ART components work inside a React DOM tree.) This makes a global flag or variable untenable.
所以,React.Component会将状态变化派发到不同平台的代码。在我们了解这些到底是怎么回事之前,我们先深入挖掘一下依赖包之间是如何分离的以及分离的原因。
大家都认为React的“引擎”存在于 react 包中。这可不对。
实际上,自从React 0.14版本包分离之后,react这个包只暴露出了一些用来定义组件的API,大部分React的实现函数都存放在渲染器中。
react-dom, react-dom/server, react-native, react-test-renderer, react-art 就是其中的一些渲染器(当然,你可以自己动手实现一个)。
这就是为什么不管你的工作环境是什么平台都需要用到react这个包,其中暴露中的函数,就像 React.Component, React.createElement, React.Children等工具函数,甚至是 Hooks,都是独立于你的目标平台的。不管你运行的是 React DOM, React DOM Server,或者 React Native,他们都是以同样的方式导入、使用react。
大部分人以为的是React的“引擎”是存在于每个不同的渲染器中。众多的渲染器中都复制了一份代码,我们管这个叫 reconciler,
What most people imagine as the React “engine” is inside each individual renderer. Many renderers include a copy of the same code — we call it the “reconciler”. A build step smooshes the reconciler code together with the renderer code into a single highly optimized bundle for better performance. (Copying code is usually not great for bundle size but the vast majority of React users only needs one renderer at a time, such as react-dom.)
The takeaway here is that the react package only lets you use React features but doesn’t know anything about how they’re implemented. The renderer packages (react-dom, react-native, etc) provide the implementation of React features and platform-specific logic. Some of that code is shared (“reconciler”) but that’s an implementation detail of individual renderers.
Now we know why both react and react-dom packages need to be updated for new features. For example, when React 16.3 added the Context API, React.createContext() was exposed on the React package.
But React.createContext() doesn’t actually implement the context feature. The implementation would need to be different between React DOM and React DOM Server, for example. So createContext() returns a few 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; } When you use <MyContext.Provider> or <MyContext.Consumer> in the code, it’s the renderer that decides how to handle them. React DOM might track context values in one way, but React DOM Server might do it differently.
So if you update react to 16.3+ but don’t update react-dom, you’d be using a renderer that isn’t yet aware of the special Provider and Consumer types. This is why an older react-dom would fail saying these types are invalid.
The same caveat applies to React Native. However, unlike React DOM, a React release doesn’t immediately “force” a React Native release. They have an independent release schedule. The updated renderer code is separately synced into the React Native repository once in a few weeks. This is why features become available in React Native on a different schedule than in React DOM.
Okay, so now we know that the react package doesn’t contain anything interesting, and the implementation lives in renderers like react-dom, react-native, and so on. But that doesn’t answer our question. How does setState() inside React.Component “talk” to the right renderer?
The answer is that every renderer sets a special field on the created class. This field is called updater. It’s not something you would set — rather, it’s something React DOM, React DOM Server or React Native set right after creating an instance of your 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; Looking at the setState implementation in React.Component, all it does is delegate work to the renderer that created this component instance:
// 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 might want to ignore a state update and warn you, whereas React DOM and React Native would let their copies of the reconciler handle it.
And this is how this.setState() can update the DOM even though it’s defined in the React package. It reads this.updater which was set by React DOM, and lets React DOM schedule and handle the update.
We know about classes now, but what about Hooks?
When people first look at the Hooks proposal API, they often wonder: how does useState “know what to do”? The assumption is that it’s more “magical” than a base React.Component class with this.setState().
But as we have seen today, the base class setState() implementation has been an illusion all along. It doesn’t do anything except forwarding the call to the current renderer. And useState Hook does exactly the same thing.
Instead of an updater field, Hooks use a “dispatcher” object. When you call React.useState(), React.useEffect(), or another built-in Hook, these calls are forwarded to the current 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); }, // ... }; And individual renderers set the dispatcher before rendering your component:
// In React DOM const prevDispatcher = React.__currentDispatcher; React.__currentDispatcher = ReactDOMDispatcher; let result; try { result = YourComponent(props); } finally { // Restore it back React.__currentDispatcher = prevDispatcher; } For example, the React DOM Server implementation is here, and the reconciler implementation shared by React DOM and React Native is here.
This is why a renderer such as react-dom needs to access the same react package that you call Hooks from. Otherwise, your component won’t “see” the dispatcher! This may not work when you have multiple copies of React in the same component tree. However, this has always led to obscure bugs so Hooks force you to solve the package duplication before it costs you.
While we don’t encourage this, you can technically override the dispatcher yourself for advanced tooling use cases. (I lied about __currentDispatcher name but you can find the real one in the React repo.) For example, React DevTools will use a special purpose-built dispatcher to introspect the Hooks tree by capturing JavaScript stack traces. Don’t repeat this at home.
This also means Hooks aren’t inherently tied to React. If in the future more libraries want to reuse the same primitive Hooks, in theory the dispatcher could move to a separate package and be exposed as a first-class API with a less “scary” name. In practice, we’d prefer to avoid premature abstraction until there is a need for it.
Both the updater field and the __currentDispatcher object are forms of a generic programming principle called dependency injection. In both cases, the renderers “inject” implementations of features like setState into the generic React package to keep your components more declarative.
You don’t need to think about how this works when you use React. We’d like React users to spend more time thinking about their application code than abstract concepts like dependency injection. But if you’ve ever wondered how this.setState() or useState() know what to do, I hope this helps.