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

React 内部原理,第三部分:基本更新

原文:React Internals, Part Three: basic updating

In part one, our small React clone, Feact, was implemented far enough to do basic rendering. But once the render happens, that was it. In this part, we’ll add the ability to make changes to the app with subsequent renders. This part will begin to show how the virtual DOM diffing works.

在第一部分,我们的小型 React 克隆(Feact)被实现的足以进行基本的渲染。在这部分中,我们将添加后续渲染对应用进行更改的功能。这部分将开始展示虚拟 DOM diffing 过程的工作原理。

The series

全部译文:

简单更新(Simple updating)

Calling setState() in a component is the primary way people cause their React apps to update. But React also supports updating through React.render(). Take this contrived example

在组件中调用 setState() 是人们更新其 React 应用程序的主要方式。但是 React 还支持通过 React.render() 进行更新。看这个例子:

React.render(<h1>hello</h1>, root);

setTimeout(function() {
React.render(<h1>hello again</h1>, root);
}, 2000);

We’ll ignore setState() for now (that’s coming in part four) and instead implement updates through Feact.render(). Truth be told, this is simply “props have changed so update”, which also happens if you render again and pass different props down to a child component. We just happen to be causing the props change through Feact.render().

我们现在将忽略 setState()(这是第四部分),而是通过 Feact.render() 实现更新。说实话,这只是简单的 “props 改变了,所以需要更新”,如果再次渲染并将不同的 props 传递给一个子组件,这也会发生。我们这里是通过 Feact.render() 导致 props 改变。

执行更新(Doing the update)

The concept is pretty simple, Feact.render() just needs to check if it has rendered before, and if so, update the page instead of starting fresh.

这个概念很简单,Feact.render() 只需要检查它之前是否已经呈现,如果是这样,更新页面而不是开始创建。

const Feact = {
...
render(element, container) {
const prevComponent = getTopLevelComponentInContainer(container);

if (prevComponent) {
return updateRootComponent(
prevComponent,
element
);
} else {
return renderNewRootComponent(element, container);
}
}
...
}

function renderNewRootComponent(element, container) {
const wrapperElement = Feact.createElement(TopLevelWrapper, element);

const componentInstance = new FeactCompositeComponentWrapper(wrapperElement);

return FeactReconciler.mountComponent(
componentInstance,
container
);
}

function getTopLevelComponentInContainer(container) {
// need to figure this out
}

function updateRootComponent(prevComponent, nextElement) {
// need to figure this out too
}

This is looking pretty promising. If we rendered before, then take the state of the previous render, grab the new desired state, and pass that off to a function that will figure out what DOM updates need to happen to update the app. Otherwise if there’s no signs of a previous render, then render into the DOM exactly how we did in part one and two.

We just need to figure out the two missing pieces.

这看起来很有保障。如果我们以前渲染过,然后采取先前渲染的状态,抓住新的需要状态,并将其传递给一个函数,由其确定哪些 DOM 需要更新并进行更新。否则,如果没有之前已经渲染的迹象,那么将其渲染到 DOM 中,就像我们在第一部分和第二部分中所做的那样。

我们只需要弄清楚两个缺失的部分。

记住我们已经做过的(Remembering what we did)

For each render, We need to store the components we created, so we can refer to them in a subsequent render. Where to store them? Why not on the DOM nodes they create?

对于每次渲染,我们需要存储我们创建的组件,以便我们可以在之后的渲染中引用它们。那么,在哪里存储它们?为什么不在他们创建的DOM节点上?

function renderNewRootComponent(element, container) {
const wrapperElement =
Feact.createElement(TopLevelWrapper, element);

const componentInstance =
new FeactCompositeComponentWrapper(wrapperElement);


const markUp = FeactReconciler.mountComponent(
componentInstance,
container
);

// new line here, store the component instance on the container
// we want its _renderedComponent because componentInstance is just
// the TopLevelWrapper, which we don't need for updates
container.__feactComponentInstance = componentInstance._renderedComponent;

return markUp;
}

Well, that was easy. Similarly retrieving the stashed component is easy too:

这很容易,类似地,找到我们隐藏的组件也很容易:

function getTopLevelComponentInContainer(container) {
return container.__feactComponentInstance;
}

更新到新的状态(Updating to the new state)

This is the simple example we are working through

这是我们使用的简单例子:

Feact.render(
Feact.createElement('h1', null, 'hello'),
root
);

setTimeout(function() {
Feact.render(
Feact.createElement('h1', null, 'hello again'),
root
);
}, 2000);

2 seconds has elapsed, so we are now calling Feact.render() again, but this time with an element that looks like

2秒过去了,我们现在再次调用 Feact.render(),但是这时候看起来像是一个元素

{
type: 'h1',
props: {
children: 'hello again'
}
}

Since Feact determined this is an update, we ended up in updateRootComponent, which is just going to delegate to the component

由于 Feact 确定这是一个更新,我们最终在 updateRootComponent 中将元素委派给组件:

function updateRootComponent(prevComponent, nextElement) {
prevComponent.receiveComponent(nextElement)
}

Notice a new component is not getting created. prevComponent is the component that got created during the first render, and now it’s going to take a new element and update itself with it. Components get created once at mount, and live on until unmount (which, does make sense…)

注意没有创建一个新组件。prevComponent 是在第一次渲染过程中创建的组件,现在它将使用一个新元素并更新它自身。组件在挂载时创建一次,直到卸载(这是有意义的)

class FeactDOMComponent {
...
receiveComponent(nextElement) {
const prevElement = this._currentElement;
this.updateComponent(prevElement, nextElement);
}

updateComponent(prevElement, nextElement) {
const lastProps = prevElement.props;
const nextProps = nextElement.props;

this._updateDOMProperties(lastProps, nextProps);
this._updateDOMChildren(lastProps, nextProps);

this._currentElement = nextElement;
}

_updateDOMProperties(lastProps, nextProps) {
// nothing to do! I'll explain why below
}

_updateDOMChildren(lastProps, nextProps) {
// finally, the component can update the DOM here
// we'll implement this next
}
};

receiveComponent just sets up updateComponent, which ultimately calls _updateDOMProperties and _updateDOMChildren which are the meaty functions which will finally cause the actual DOM to get updated. _updateDOMProperties is mostly concerned with updating CSS styles. We’re not going to implement it in this blog post series, but just pointing it out as that is the method React uses to deal with style changes.

receiveComponent 只是设置 updateComponent ,它最终调用 _updateDOMProperties_updateDOMChildren,它们是最终导致实际DOM更新的实体函数。_updateDOMProperties 主要关心更新 CSS 样式。我们不会在这个博客文章系列中实现它,只是指出它是 React 用于处理样式更改的方法。

_updateDOMChildren in React this method is pretty complex, handling a lot of different scenarios. But in Feact the children is just the text contents of the DOM element, in this case the children will go from "hello" to "hello again"

在 React中 _updateDOMChildren 这个方法很复杂,处理了很多不同的场景。但在 Feact 中,子元素只是 DOM 元素的文本内容,在这种情况下,子元素将从 "hello" 变成 "hello again"

class FeactDOMComponent {
...
_updateDOMChildren(lastProps, nextProps) {
const lastContent = lastProps.children;
const nextContent = nextProps.children;

if (!nextContent) {
this.updateTextContent('');
} else if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
}
}

updateTextContent(text) {
const node = this._hostNode;

const firstChild = node.firstChild;

if (firstChild && firstChild === node.lastChild
&& firstChild.nodeType === 3) {
firstChild.nodeValue = text;
return;
}

node.textContent = text;
}
};

Feact‘s version of _updateDOMChildren is hopelessly stupid, but this is all we need for our learning purposes.

Feact 的版本的 _updateDOMChildren 是非常愚蠢的,但它对于我们的学习目标很有帮助。

更新复合组件(Updating composite components)

The work we did above was fine and all, but we can only update FeactDOMComponents. In other words, this won’t work

我们上面所做的已经很好,但是只能更新 FeactDOMComponent,也就是说,还不能工作在如下场景:

Feact.render(
Feact.createElement(MyCoolComponent, { myProp: 'hello' }),
document.getElementById('root')
);

setTimeout(function() {
Feact.render(
Feact.createElement(MyCoolComponent, { myProp: 'hello again' }),
document.getElementById('root')
);
}, 2000);

Updating composite components is much more interesting and where a lot of the power in React lies. The good news is, a composite component will ultimately boil down to a FeactDOMComponent, so all the work we did above won’t go to waste.

更新复合组件非常有趣,而这是 React 的大部分工作。好消息是,复合组件最终将归结为 FeactDOMComponent,所以我们上面所做的工作不会浪费。

Even more good news, updateRootComponent has no idea what kind of component it received. It just blindly calls receiveComponent on it. So all we need to do is add receiveComponent to FeactCompositeComponentWrapper and we’re good!

更好的消息是,updateRootComponent 不知道接收到什么样的组件。它只是盲目地调用 receiveComponent。所以我们需要做的是将 receiveComponent 添加到 FeactCompositeComponentWrapper,这很容易!

class FeactCompositeComponentWrapper {
...
receiveComponent(nextElement) {
const prevElement = this._currentElement;
this.updateComponent(prevElement, nextElement);
}

updateComponent(prevElement, nextElement) {
const nextProps = nextElement.props;

this._performComponentUpdate(nextElement, nextProps);
}

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

inst.props = nextProps;

this._updateRenderedComponent();
}

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

prevComponentInstance.receiveComponent(nextRenderedElement);
}
}

It’s a little silly to spread such little logic across four methods, but it will make more sense as we progress. These four methods are also what is found in React’s ReactCompositeComponentWrapper.

在这四种方法中传递这样的小逻辑看起来有点愚蠢,但将因为我们的进展变得很有意义。这四种方法也可以在 React 的 ReactCompositeComponentWrapper 中找到。

Ultimately the update boils down to calling render with the current set of props. Take the resulting element and passing it on to the _renderedComponent, and telling it to update. _renderedComponent could be another FeactCompositeComponentWrapper, or possibly a FeactDOMComponent. It was created during the first render.

最终,调用 render 更新归结为使用当前的 props 来渲染,取得结果元素并将其传递给 _renderedComponent ,通知它进行更新。 _renderedComponent 可以是 FeactCompositeComponentWrapper,或者可能是 FeactDOMComponent。它是第一次渲染过程中创建的。

让我们再次使用 FeactReconciler(Let’s use FeactReconciler again)

Mounting components always goes through FeactReconciler, so updating them should to. This isn’t that important for Feact, but it keeps us consistent with React.

安装组件总是通过 FeactReconciler,因此也应该更新它们。这对于 Feact 并不重要,但可以与 React 保持一致。

const FeactReconciler = {
...
receiveComponent(internalInstance, nextElement) {
internalInstance.receiveComponent(nextElement);
}
};


function updateRootComponent(prevComponent, nextElement) {
FeactReconciler.receiveComponent(prevComponent, nextElement);
}

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

FeactReconciler.receiveComponent(
prevComponentInstance, nextRenderedElement);
}
}

shouldComponentUpdate和componentWillReceiveProps(shouldComponentUpdate and componentWillReceiveProps)

We can now easily add these two lifecycle methods into Feact.

我们现在可以轻松地将这两种生命周期方法添加到 Feact 中。

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

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

let shouldUpdate = true;

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

if (shouldUpdate) {
this._performComponentUpdate(nextElement, nextProps);
} else {
// if skipping the update,
// still need to set the latest props
inst.props = nextProps;
}
}
...
}

A Major Hole

There’s a big problem with Feact’s updating that we won’t be addressing. It’s making the assumption that when the update happens, it can keep using the same type of component.

In other words, Feact can handle this just fine

Feact 的更新有一个很大的问题,我们没有指出。这些都是建立在,当更新发生时,它仍然继续使用相同类型的组件的假设上的。

换句话说,Feact 可以很好的处理这个问题:

Feact.render(
Feact.createElement(MyCoolComponent, { myProp: 'hi' }),
root
);

// some time passes

Feact.render(
Feact.createElement(MyCoolComponent, { myProp: 'hi again' }),
root
);

but it can’t handle this

但是,它无法处理这样的问题:

Feact.render(
Feact.createElement(MyCoolComponent, { myProp: 'hi' }),
root
);

// some time passes

Feact.render(
Feact.createElement(SomeOtherComponent, { someOtherProp: 'hmmm' }),
root
);

In this case, the update swapped in a completely different component class. Feact will just naively grab the previous component, which would be a MyCoolComponent, and tell it to update with the new props { someOtherProp: 'hmmm'}. What it should have done is notice the component type changed, and instead of updating, unmounted MyCoolComponent and mounted SomeOtherComponent.

在这种情况下,更新被交给一个完全不同的组件类中。 Feact 只是简单地抓住以前的组件,比如这是 MyCoolComponent,使用新的 props { someOtherProp: 'hmmm'} 告诉它去更新。它还应该做的是注意组件类型已更改,卸载旧的 MyCoolComponent 并挂载了 SomeOtherComponent,而不是更新 MyCoolComponent 组件。

In order to do this, Feact would need:

  • some ability to unmount a component
  • notice the type change and head over to FeactReconciler.mountComponent instead of FeactComponent.receiveComponent

为了做到这一点,Feact将需要:

  • 卸载组件的能力
  • 注意到组件类型更改,并跳转到 FeactReconciler.mountComponent 而不是 FeactComponent.receiveComponent

In React, if you render again with the same component type, it will get updated. You don’t actually need to specify a key for your element to update in most cases. Keys are only necessary when a component is dealing with a collection of children. In this case, React will warn you if you forget your keys. It’s best to heed the warning, because without the key React is not updating, but completely unmounting and mounting again!

在React中,如果使用相同的组件类型再次渲染,则会被更新。在大多数情况下,您实际上不需要指定要更新的元素的 key。只有当组件处理子元素集合时才需要key。在这种情况下,如果您忘记了 key,React 会警告您。最好注意警告,因为没有 key 的 React 将不会更新,而是完全卸载和重新挂载!

你发现了虚拟 DOM 吗?(Did you spot the virtual DOM?)

When React first came out, a lot of the hype was around the “virtual DOM”. But the virtual DOM isn’t really a concrete thing. It is more a concept that all of React (and Feact) accomplish together. There isn’t anything inside React called VirtualDOM or anything like that. Instead prevElement and nextElement together capture the diff from render to render, and FeactDOMComponent applies the diff into the actual DOM.

当 React 第一次出现时,大量的炒作都围绕着“虚拟DOM”。但虚拟 DOM 并不是一个具体的内容,它更像所有的 React(和 Feact 等类似框架)完成其内容的一个概念。React 里面没有任何内容叫做 VirtualDOM 或类似这样的东西。相反,prevElementnextElement 一起捕获两次渲染的差异,FeactDOMComponent 将差异应用于实际的DOM。

总结(Conclusion)

And with that, Feact is able to update components, albeit only through Feact.render(). That’s not too practical, but we’ll improve things next time when we explore setState().

现在,Feact 可以更新组件,尽管只能通过 Feact.render() 更新。这并不太实用,但是下一次我们探索 setState() 时,会改进。

To wrap things up, here is a fiddle encompassing all that we’ve done so far

这里是一个 fiddle 在线示例,包括我们迄今所实现的内容:

fiddle

On to part four!