超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

React 内部原理,第四部分:setState

原文:React Internals, Part Four: setState

In part four of this series we finally add setState to our little React clone. setState is a beast, so grab your favorite beverage and get comfortable!

The series

全部译文:

为 Feact 添加 State(Adding state to Feact)

State and props are very similar in that they are both data and both influence how and when a component renders. The core difference is props come from an outside source, where state is entirely internal to the component. So far Feact only supports props, so before we can implement setState we need to add the notion of state itself to the framework.

state 和 props 非常相似,因为它们都是影响组件渲染的时间和方式的数据。核心区别是 props 来自组件外部,而状态完全在组件内部。到目前为止,Feact 只支持 props,所以在我们可以实现 setState 之前,我们需要在框架中添加 state 的概念。

getInitialState

When mounting a fresh component, we need to set up its initial state, that’s where this lifecycle method comes in. It’s just called when a component is getting instantiated, so we need to hook into this method in the constructor function that Feact.createClass creates

当挂载新的组件时,我们需要设置它的初始状态,这是生命周期方法开始的地方。当组件被实例化时,它被调用,所以我们需要在 Feact.createClass 创建的构造函数中执行这个方法。

const Feact = {
createClass(spec) {
function Constructor(props) {
this.props = props;

// new lines added for state
const initialState = this.getInitialState ?
this.getInitialState() :
null;
this.state = initialState;
}

Constructor.prototype =
Object.assign(Constructor.prototype, spec);

return Constructor;
}
}

Just like props, we set the state on the instance.

就像 props 一样,我们在实例上设置 state。

Notice if the component does not have getInitialState defined, the initial state will be null? React won’t default initial state to an empty object, so if you want to use state, chances are you need to implement this method and return an object, otherwise your first render will blow up if it tries to do this.state.foo

注意,如果组件没有定义 getInitialState,初始状态将为 null? React 不会将默认初始状态设置为空对象,所以如果要使用状态,则可能需要实现此方法并返回一个对象,否则,如果尝试执行 this.state.foo,您的第一个渲染将会错误。

Now with getInitialState defined, Feact components can start using this.state whenever they’d like.

现在 getInitialState 已经定义,Feact 组件可以随时开始使用 this.state。

添加一个简单的(Adding a simple setState())

Whenever a component wants to update, it needs to tell Feact “hey, I’d like to render again!”, and this.setState() is the primary way to accomplish that. setState updates this.state, and triggers a render, which will send the component through the lifecycle methods shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate (which Feact doesn’t have, but of course React does).

每当一个组件想要更新,它需要告诉 Feact “嘿,我想再次渲染”,而 this.setState() 是完成这个的主要方式。setState 更新 this.state,并触发一个渲染,它将通过生命周期方法 shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate(该 Feact没有,但是 React 有)发送组件。

在组件上定义 setState(Defining setState on the component)

Again we need to tweak Feact.createClass to get setState in place. To do this, we’ll give all classes created this way a prototype, and this prototype will have setState defined

我们需要再次调整 Feact.createClass 来获取 setState。为此,我们将给出所有以这种方式创建的类一个原型,而这个原型上定义了 setState

function FeactComponent() {
}

FeactComponent.prototype.setState = function() {
// to be implemented later
};

function mixSpecIntoComponent(Constructor, spec) {
const proto = Constructor.prototype;

for (const key in spec) {
proto[key] = spec[key];
}
}

const Feact = {
createClass(spec) {
function Constructor(props) {
this.props = props;

// new lines added for state
const initialState = this.getInitialState ? this.getInitialState() : null;
this.state = initialState;
}

Constructor.prototype = new FeactComponent();

mixSpecIntoComponent(Constructor, spec);
return Constructor;
}
}

Prototypical inheritance in action. mixSpecIntoComponent in React is more complicated (and robust), dealing with things like mixins and making sure users don’t accidentally clobber a React method.

原型继承在起作用。mixSpecIntoComponent 在 React 中更复杂(强大),处理像 mixins 这样的事情,确保用户不会意外地破坏 React 方法。

将 setState 绑定到 updateComponent(Threading setState over to updateComponent)

Back in part three we updated a component by calling FeactCompositeComponentWrapper#receiveComponent, which in turn called updateComponent. It makes sense to not repeat ourselves, so we should thread state updates through updateComponent too. We need to get all the way from FeactComponent.prototype.setState to FeactCompositeComponentWrapper#updateComponent. Currently Feact has no means of accomplishing this.

回到第三部分,我们通过调用 FeactCompositeComponentWrapper#receiveComponent 来更新一个组件,它又反过来调用 updateComponent。没必要再重复一遍,所以我们也应该通过 updateComponent 进行状态更新。我们需要从 FeactComponent.prototype.setStateFeactCompositeComponentWrapper#updateComponent。目前,无法 Feact 完成这项工作。

In React, there is the notion of “public instances” and “internal instances”. Public instances are the objects that get created from the classes defined with createClass, and internal instances are the objects that React internally creates. In this scenario the internal instance is the FeactCompositeComponentWrapper that the framework created. The internal instance knows about the public instance, since it wraps it. But the relationship doesn’t go in the opposite direction, yet now it needs to. Here setState is the public instance attempting to communicate with the internal instance, so with that in mind, let’s take a stab at implementing setState

在 React 中,存在“公共实例”和“内部实例”的概念。公共实例是从使用 createClass 定义的类创建的对象,内部实例是 React 内部创建的对象。在这种情况下,内部实例是框架创建的 FeactCompositeComponentWrapper 。内部实例知道公共实例,因为它包装的公共实例。但是,这种关系是单向的,现在需要双向的关系,也就是说我们需要公共示例知道内部示例。这里的 setState 是尝试与内部实例进行通信的公共实例,所以考虑到这一点,我们来强制实现 setState

function FeactComponent() {
}

FeactComponent.prototype.setState = function(partialState) {
const internalInstance = getMyInternalInstancePlease(this);

internalInstance._pendingPartialState = partialState;

FeactReconciler.performUpdateIfNecessary(internalInstance);
}

React solves the “get my internal instance” problem with an instance map, which really just stores the internal instance on the public instance

React使用实例映射解决了“获取我的内部实例”的问题,它实际上只是将内部实例存储在公共实例上。

const FeactInstanceMap = {
set(key, value) {
key.__feactInternalInstance = value;
},

get(key) {
return key.__feactInternalInstance;
}
};

We’ll set up this relationship while mounting

我们将在组件挂载时建立这种关系。

class FeactCompositeComponentWrapper {
...
mountComponent(container) {
const Component = this._currentElement.type;
const componentInstance = new Component(this._currentElement.props);
this._instance = componentInstance;

FeactInstanceMap.set(componentInstance, this);
...
}
}

We have one other unimplemented method, FeactReconciler.performUpdateIfNecessary, but just like other reconciler methods, it will just delegate to the instance

我们有一个其他未实现的方法,FeactReconciler.performUpdateIfNecessary,但是像其他 reconciler 方法一样,它将只是委托给实例。

const FeactReconciler = {
...
performUpdateIfNecessary(internalInstance) {
internalInstance.performUpdateIfNecessary();
}
...
}

class FeactCompositeComponentWrapper {
...
performUpdateIfNecessary() {
this.updateComponent(this._currentElement, this._currentElement);
}
...
}

Finally, we are calling updateComponent! Notice we seem to be cheating a little bit. We are saying to update the component, but with the same element being used as both previous and next. Whenever updateComponent is called with the same element, then React knows only state is getting updated, otherwise props are updating. React will decide whether to call componentWillReceiveProps based on prevElement !== nextElement, so let’s go ahead and throw that into Feact too

最后我们调用 updateComponent !请注意,我们似乎在隐瞒了一点。我们说要更新组件,但是使用相同的元素作为上一个和下一个更新的组件。每当使用相同的元素调用 updateComponent 时,React 只知道 state 正在更新,否则 props 正在更新。 React 会决定是否根据 prevElement !== nextElement 调用 componentWillReceiveProps,所以让我们继续把它转换成 Feact

class FeactCompositeComponentWrapper {
...
updateComponent(prevElement, nextElement) {
const nextProps = nextElement.props;
const inst = this._instance;

const willReceive = prevElement !== nextElement;

if (willReceive && inst.componentWillReceiveProps) {
inst.componentWillReceiveProps(nextProps);
}
...
}
}

That isn’t the entirety of updateComponent, (check the fiddle at the end of the article for all the code), just enough to show that calling setState() does not cause componentWillReceiveProps to get called before the render happens. Which does make sense, setState has no means of influencing props, just state.

这并不是完整的 updateComponent (在文章底部的 fiddle 获得所有代码),但足以显示调用 setState() 不会导致 componentWillReceiveProps 在渲染发生之前被调用。这是有道理的, setState 没有办法影响 props,只是影响 state。

If you want a heads up on every render, whether caused by prop changes or state changes, then implement componentWillUpdate in your component. We won’t add it to Feact since this blog series is already too long, but it’s called right before a render, no matter what caused the render. The only exception is the first render, where you can hook into componentWillMount instead.

如果您想考虑每个渲染上是否发生,无论是由 props 更改或 state 更改引起的,请在组件中实现 componentWillUpdate。我们不会将它添加到 Feact中,因为这个博客系列已经太长了,但是无论什么导致了渲染,它都将在渲染之前就被调用。唯一的例外是第一次渲染,但这种情况下您也可以在 componentWillMount钩子中执行一些操作。

用新的 state 更新(Updating with the new state)

If you trace through the code we’ve written so far, you’ll see we’re now hanging out in updateComponent, and the internal instance has the pending partial state waiting to be used at internalInstance._pendingPartialState. Now all we need to do is have the component render again – this time with state –, then from there actually getting the update all the way into the DOM is the same procedure as done back in part three

如果您浏览到目前为止写的代码,您将看到我们现在在 updateComponent 中进行操作,并且在内部实例有等待在 internalInstance._pendingPartialState 中使用的部分状态。现在我们需要做的是使组件再次渲染,这次与状态有关,然后实际更新到 DOM 的方式是完全和第三部分中相同的过程。

class FeactCompositeComponentWrapper {
...
updateComponent(prevElement, nextElement) {
const nextProps = nextElement.props;
const inst = this._instance;

const willReceive = prevElement !== nextElement;

if (willReceive && inst.componentWillReceiveProps) {
inst.componentWillReceiveProps(nextProps);
}

let shouldUpdate = true;
const nextState =
Object.assign({}, inst.state, this._pendingPartialState);
this._pendingPartialState = null;

if (inst.shouldComponentUpdate) {
shouldUpdate =
inst.shouldComponentUpdate(nextProps, nextState);
}

if (shouldUpdate) {
this._performComponentUpdate(
nextElement, nextProps, nextState
);
} else {
inst.props = nextProps;
inst.state = nextState;
}
}

_performComponentUpdate(nextElement, nextProps, nextState) {
this._currentElement = nextElement;
const inst = this._instance;

inst.props = nextProps;
inst.state = nextState;

this._updateRenderedComponent();
}

_updateRenderedComponent() {
const prevComponentInstance = this._renderedComponent;
const inst = this._instance;
const nextRenderedElement = inst.render();

FeactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement
);
}
...
}

This updating of the component is almost identical to part three, with the exception of the added state of course. Since state just sits on the public instance at this.state, _performComponentUpdate only had a one line change and _updateRenderedComponent had no change at all. The real key change was in the middle of updateComponent where we merge the previous state with the new partial state, and this partial state originated way back in this.setState().

组件的更新几乎与第三部分相同,当然,除了新添加的 state。由于 state 只是在公共实例的 this.state 上,_performComponentUpdate 只有一行更改,而 _updateRenderedComponent 根本没有改变。真正的关键变化是在 updateComponent,我们将先前的 state 与新的局部 state 来自 this.setState() 的 state 进行合并。 全做完了!

全做完了! … 对吗?(All done! … right?)

Phew, we now have setState! Here is a fiddle of what we have so far

现在我们已经有了 setState ,这里是迄今为止的完整代码:

fiddle

But Feact’s setState is a bit simple, not performant and could even be a little surprising. The main problem is every distinct call to setState causes the component to render. This forces the user to either figure out how to fit all their changes into one call, or accept that each call will render. It’d be better if the programmer could call setState when it’s convenient, and let the framework batch the calls when it can, resulting in fewer renders.

但是,Feact 的 setState 有点简单,没有性能,甚至有点令人惊讶。主要的问题是每个不同的调用 setState 导致组件呈现。这将迫使用户确定如何将其所有更改合并为一次调用,或接受每次调用都将渲染。如果程序员可以在方便的时候调用 setState,那么这个框架会更好,让框架在调用时可以批量调用,从而产生更少的渲染。

批处理 setState 调用(Batching setState calls)

If you take a look at Feact’s render lifecycle, you can see we call componentWillReceiveProps just before we’re about to render. What if inside componentWillReceiveProps the user was to call setState? Currently in Feact, that’d cause it to go ahead and start a second render, while in the middle of the first render! That doesn’t sound good. Not to mention, responding to incoming props by updating your state is a common need. It makes sense to expect your state update and the new props to all flow into the same render, otherwise you’d get an intermediate render with only the state change, then the final render with both state and props change, which would probably be unexpected.

如果您看看 Feact 的渲染生命周期,可以看到我们在渲染之前调用 componentWillReceiveProps。如果用户在 componentWillReceiveProps 内部要调用 setState 怎么办?目前在 Feact 中,这将导致它继续执行,并在第一个渲染的中间开始第二个渲染!这听起来不好,更不要说,通过响应传入的 state 来更新 props 是一个常见的需求。期望你的 state 更新和新的 props 流入同一渲染是有道理的,否则你会得到只有 state 改变的中间渲染,然后 state 和 props 同时改变的最终渲染,可能是不可预期的。

Here is a fiddle that demonstrates this (fiddle). Depending on your browser, you might not be able to see the second render. But if you open the debugger and place a debugger; statement in FeactDOMComponent#_updateTextContent, you should be able to see how Feact naively does three renders when it should have been just two.

这是一个 fiddle 的演示(fiddle )。根据您的浏览器,您可能无法看到第二次渲染。但是如果您打开调试器并放置断点在 FeactDOMComponent#_updateTextContent 中,您应该能够看到三次呈现,Feact 如何天真地做了三次渲染,而本应该只是两次渲染的。

批量步骤一,存放批量状态变化的地方(batching step one, a place to store the batched state changes)

We need a place to store more than one state update, so we will change _pendingPartialState into an array

我们需要一个存储多个状态更新的地方,所以我们将 _pendingPartialState 更改为一个数组。

function FeactComponent() {
}

FeactComponent.prototype.setState = function(partialState) {
const internalInstance = FeactInstanceMap.get(this);

internalInstance._pendingPartialState =
internalInstance._pendingPartialState || [];

internalInstance._pendingPartialState.push(partialState);
...
}

Over in updateComponent, let’s pull the state processing out into its own method

updateComponent 中,我们将状态处理拉出到自己的方法中。

class FeactCompositeComponentWrapper {
...
updateComponent(prevElement, nextElement) {
...
const nextState = this._processPendingState();
...
}

_processPendingState() {
const inst = this._instance;
if (!this._pendingPartialState) {
return inst.state;
}

let nextState = inst.state;

for (let i = 0; i < this._pendingPartialState.length; ++i) {
nextState =
Object.assign(nextState, this._pendingPartialState[i]);
}

this._pendingPartialState = null;
return nextState;
}
}

批量步骤二,将状态更改合为一个渲染(batching step two, batching up the state changes into one render)

The batching mechanism we’re about to add to Feact is very simple and not at all what React does. The point is to just show the general idea of how batching works (and later, show why it can make setState tricky).

我们即将添加到“快乐”中的配料机制非常简单,而且还没有反应。关键是要显示批处理如何工作的一般想法(后来,显示为什么它可以使setState变得棘手)。

For Feact, we will batch updates while rendering, otherwise, we won’t batch them. So during updateComponent, we just set a flag that tells the world we are rendering, then unset it at the end. If setState sees we are rendering, it will set the pending state, but not cause a new render, as it knows the current render that is going on will pick up this state change

对于 Feact,我们将在渲染时批量更新,否则我们不会批量更新。所以在 updateComponent 期间,我们只是设置一个标志告诉世界我们正在渲染,然后在渲染的最后取消设置。如果 setState 看到我们正在渲染,它将设置待处理状态,但不会导致新的渲染,因为它知道当前正在执行的渲染将会接收到该状态更改。

class FeactCompositeComponentWrapper {
...
updateComponent(prevElement, nextElement) {
this._rendering = true;

// entire rest of the method

this._rendering = false;
}
}

function FeactComponent() {
}

FeactComponent.prototype.setState = function(partialState) {
const internalInstance = FeactInstanceMap.get(this);

internalInstance._pendingPartialState = internalInstance._pendingPartialState || [];

internalInstance.push(partialState);

if (!internalInstance._rendering) {
FeactReconciler.performUpdateIfNecessary(internalInstance);
}
}

包装起来(wrapping it up)

Here is a fiddle that contains the final version of Feact:

这是一个包含最终版本的 Feact 的 fiddle:

fiddle

It contains the simple batching, so it will only render twice (whereas the previous fiddle above rendered three times).

它包含简单的批处理,所以它只会渲染两次(而上一个 fiddle 渲染三次)。

setState 陷阱(setState pitfalls)

Now that we understand how setState works and the overall concept on how batching works, we can see there are some pitfalls in setState. The problem is it takes several steps to update a component’s state, as each pending partial state needs to get applied one by one. That means using this.state when setting state is dangerous

现在我们了解了 setState 的工作原理以及批处理如何工作的总体概念,我们可以看到 setState 有一些陷阱。问题是需要几个步骤更新组件的状态,因为每个待处理的部分状态需要逐个应用。这意味着在设置状态的时候使用 this.state 是很危险的。

componentWillReceiveProps(nextProps) {
this.setState({ counter: this.state.counter + 1 });
this.setState({ counter: this.state.counter + 1 });
}

This contrived example shows what I mean. You might expect counter to get 2 added to it, but since states are being batched up, the second call to setState has the same values for this.state as the first call, so counter will only get incremented once.

React solves this problem by allowing a callback to be passed into setState

这个例子显示了我的意思。您可能希望计数器可以添加2,但是由于状态正在被批量化,所以第二次调用 this.state this.state 的值与第一次调用 this.state 的值相同,所以计数器只会增加一次。 React 通过允许将回调函数传递给 this.state 来解决这个问题

componentWillReceiveProps(nextProps) {
this.setState((currentState) => ({
counter: currentState.counter + 1
});
this.setState((currentState) => ({
counter: currentState.counter + 1
});
}

By using the callback flavor of setState, you get access to the intermediate values state works through. If Feact were to implement this, it’d look like

通过使用 setState 的回调函数,您可以访问工作中的 state 值。如果 Feact 要实现这一点,看起来就像:

_processPendingState() {
const inst = this._instance;
if (!this._pendingPartialState) {
return inst.state;
}

let nextState = inst.state;

for (let i = 0; i < this._pendingPartialState.length; ++i) {
const partialState = this._pendingPartialState[i];

if (typeof partialState === 'function') {
nextState = partialState(nextState);
} else {
nextState = Object.assign(nextState, patialState);
}
}

this._pendingPartialState = null;
return nextState;
}

You can see how the callback gets access to the intermediate values of nextState as we work our way through all the pending changes.

你可以看到回调函数如何访问 nextState 的中间值,当我们我们正在等待完成所有待处理的更改。

下一步(Up Next)

If you’ve read this far then holy cow, thanks! Feel free to email me if you have any feedback.

Part five is just around the corner. It will go over React’s transactions and wrap the whole shebang up. Stay tuned.

Here is the final fiddle for Feact one more time:

fiddle