前端优化反应: 虚拟dom解释

了解反应的虚拟dom,并使用此知识加快应用程序。在这个全面入门的框架内部入门中,我们将揭开JSX的神秘化,让您展示如何做出反应,解释如何找到瓶颈,并分享一些避免常见错误的提示。

反应的原因之一一直动摇着前端世界,并没有下降的迹象,它平易近人的学习曲线:在你绕着头后,学习曲线。n.JSX还有整个“国家vs.道具“概念,你可以走了。

如果你已经熟悉了反应工作的方式,你可以直接跳到“修理东西”.

但是要真正掌握自己的反应,你需要思考反应。这篇文章是想帮你解决这个问题。看看所做的反应表我们的项目之一:

前端优化反应: 虚拟dom解释_第1张图片

一个巨大的反应表ebay业务.

使用数百条动态的、多层的行,理解框架的细点对于保证用户体验的顺利进行至关重要。

当事情发生的时候你肯定会感觉到。输入字段会得到laggy,复选框会先检查一下,情态动词会出现困难的时候。

为了解决这些问题,我们需要覆盖整个旅程,一个反应组件从定义到您定义(然后更新)页面上。系好安全带!

在JSX后面

过程中已知的前端圈为“transpiling”,即使“编译”将是一个更正确的术语。

反应开发人员敦促您在编写组件时使用名为JSX的html和javascript组合。然而,浏览器对于JSX及其语法没有任何线索。浏览器只理解简单javascript,所以必须将JSX转换成它。下面是一个div它有一个类和一些内容:

 className='cn'>
  Content!

“正式”javascript中的相同代码只是一个带有若干参数的函数调用:

React.createElement(
  'div',
  { className: 'cn' },
  'Content!'
);

让我们仔细看看这些论点。二是一个元素类型。对于html标记,它将是一个带有标记名的字符串。第二个参数是一个对象,它包含所有元素属性。如果没有空对象,它也可以是一个空对象。下面所有的论点都是元素的孩子。元素中的文本也作为子元素计数,因此字符串“内容!”作为函数调用的第三个参数放置。

你已经可以想象当我们有更多孩子时会发生什么:

 className='cn'>
  Content 1!
   />
  Content 2!
React.createElement(
  'div',
  { className: 'cn' },
  'Content 1!',              // 1st child
  React.createElement('br'), // 2nd child
  'Content 2!'               // 3rd child
)

我们的函数现在有五个参数:元素类型、属性对象和三个子元素。因为我们的一个孩子也是一个众所周知的反应,它将被描绘成一个函数调用。

现在,我们已经覆盖了两种类型的儿童:平原String或者另一个电话React.createElement。然而,其他价值也可以作为论据:

  • 基元falsenullundefined以及true
  • 阵列
  • 反应组件

数组是用来作为一个参数分组并传递的子元素:

React.createElement(
  'div',
  { className: 'cn' },
  ['Content 1!', React.createElement('br'), 'Content 2!']
)

当然,反应的力量来自于html规范中描述的标签,但是来自用户创建的组件,例如:

function Table({ rows }) {
  return (
    <table>
      {rows.map(row => (
        <tr key={row.id}>
          <td>{row.title}</td>
        </tr>
      ))}
    </table>
  );
}

组件允许我们将模板破坏成可重用的块。在一个示例中,“功能”上面的组件接受数组行数据的对象数组,并返回单个React.createElement请呼叫

元素及其行作为子。

每当我们把组件放置到这样的布局中:

 rows={rows} />

从浏览器的角度来看,我们写了这篇文章:

  React.createElement(Table, { rows: rows });

注意,这次我们的第一个参数不是String描述一个html元素,但是对我们定义的函数的引用当我们编码我们的组件时。我们的属性现在是我们的props.

将组件放在页面上

所以,我们已经将所有的JSX组件都转换成纯javascript,现在我们有了一系列函数调用,其中还有其他函数调用,还有其他函数调用…如何将它们转换成构成web页面的dom元素?

为了这个,我们有一个ReactDOM图书馆及其render方法:

function Table({ rows }) { /* ... */ } // defining a component

// rendering a component
ReactDOM.render(
  React.createElement(Table, { rows: rows }), // "creating" a component
  document.getElementById('#root') // inserting it on a page
);

何时ReactDOM.render被称为React.createElement最后调用了它,它返回以下对象:

// There are more fields, but these are most important to us
{
  type: Table,
  props: {
    rows: rows
  },
  // ...
}

这些对象构成了虚拟dom在反应上的意义。

它们将在所有进一步渲染中相互比较,最终转换为dom(与虚拟).

下面是另一个例子:这次使用div具有类属性和若干子属性:

React.createElement(
  'div',
  { className: 'cn' },
  'Content 1!',
  'Content 2!',
);

变成:

{
  type: 'div',
  props: {
    className: 'cn',
    children: [
      'Content 1!',
      'Content 2!'
    ]
  }
}

注意,过去使用的是单独的参数React.createElement函数在a/s之下找到了它们的位置children内钥匙props。所以它无所谓如果孩子作为数组或参数列表传递-在生成的虚拟dom对象中,它们最终都会一起结束。

此外,我们可以将孩子直接添加到在代码中,结果仍然是一样的:

 className='cn' children={['Content 1!', 'Content 2!']} />

构建了虚拟dom对象之后,ReactDOM.render将尝试将其转换为我们浏览器可以根据这些规则显示的Dom节点:

  • 如果type属性持有使用标记名称-创建一个标记,其中列出了下面列出的所有属性props.

  • 如果我们有一个函数或类type-调用它并递归地重复一个结果。

  • 如果有什么childrenprops-逐个重复这个过程,并将结果放置在父节点的Dom节点内。

因此,我们得到以下html(对于我们的表示例):

...
Title

重建在

在实践中,render通常会在根元素上调用一次,并且进一步更新通过state.

注意:“重新”在标题中!当我们想要做的时候,真正的反应就开始了更新一页没有取代一切。我们怎么能做到这一点也没有什么办法。让我们从最简单的一个调用开始ReactDOM.render对于同一个节点再一次.

// Second call
ReactDOM.render(
  React.createElement(Table, { rows: rows }),
  document.getElementById('#root')
);

这次,上面的代码将与我们已经看到的不同。响应将从零开始创建所有Dom节点,并将它们放到页面上,而响应将开始和解(或“diffing”)算法来确定必须更新节点树的哪些部分,并且可以保持未受影响。

那么,它是如何工作的呢?只有几个简单的场景和理解他们我们的优化将会帮助我们很多。请记住,我们现在正在查看作为响应虚拟dom中节点的表示形式的对象。

  • 设想1:type是一个字符串,type在电话里保持相同的距离props也没有改变。
// before update
{ type: 'div', props: { className: 'cn' } }

// after update
{ type: 'div', props: { className: 'cn' } }

这是最简单的例子:dom保持不变。

  • 设想2:type仍然是同一个字符串,props是不同的。
// before update:
{ type: 'div', props: { className: 'cn' } }

// after update:
{ type: 'div', props: { className: 'cnn' } }

作为我们type仍然表示html元素响应知道如何通过标准的Dom调用更改其属性,而无需从一种树中删除节点。

  • 设想3:type已经变了不同String或从String到组件上。
// before update:
{ type: 'div', props: { className: 'cn' } }

// after update:
{ type: 'span', props: { className: 'cn' } }

当响应现在看到类型不同时,它甚至不会尝试更新我们的节点:旧元素将会被删除(下装和所有的孩子一起。因此,对于完全不同的高级别dom树的元素替换一个元素可能非常昂贵。幸运的是,在现实世界里很少发生这种事。

记住反应用途是很重要的===(三倍等于)比较type值,所以它们必须是相同的实例同一类或功能。

接下来的场景更有趣,因为这就是我们经常使用反应的方式。

  • 情景4:type是一个组件。
// before update:
{ type: Table, props: { rows: rows } }

// after update:
{ type: Table, props: { rows: rows } }

“可是什么都没变!”你也许会说,你会错的。

注意组件的render(只有类组件已显式定义了此方法),但与ReactDOM.render。“渲染”这个词的确在反应世界中确实被过度使用了。

如果type是对函数或类(即正则反应组件)的引用,并且我们开始了树的调整过程,然后反应总是试图看组件以确保返回的值返回render没有改变(预防副作用的一种预防)。冲洗和重复每一个组件下树-是的,复杂的渲染也可能变得昂贵!

照顾孩子

除了上面描述的四个常见场景外,我们还需要考虑当元素有多个子时的响应行为。我们假设我们有这样一个元素:

// ...
props: {
  children: [
      { type: 'div' },
      { type: 'span' },
      { type: 'br' }
  ]
},
// ...

我们想把这些孩子们洗牌:

// ...
props: {
  children: [
    { type: 'span' },
    { type: 'div' },
    { type: 'br' }
  ]
},
// ...

然后呢?

如果“diffing”,反应就会看到任何内部阵列props.children它开始比较它中的元素与它之前看到的数组中的元素,然后依次查看它们:索引0将与索引0、索引1和索引1等进行比较。对于每一对,反应将应用上面描述的规则集。在我们的例子中,它看到div变成了一个span所以设想3将被应用。这并不是非常有效的:想象一下,我们已经从1000行表中删除了第一行。反应将不得不“更新”剩余999名儿童,因为他们的内容现在不会相等,如果与先前的代表指数相比,则是相等的。

幸运的是,反应有内建来解决这个问题。如果元素具有key属性将比较元素的值。key不是按指数来的。只要钥匙是独一无二的,反应就会围绕着元素移动将它们从Dom树中移除然后将它们放到后面(在响应中已知的过程为安装/卸载).

// ...
props: {
  children: [ // Now React will look on key, not index
    { type: 'div', key: 'div' },
    { type: 'span', key: 'span' },
    { type: 'br', key: 'bt' }
  ]
},
// ...

当状态发生变化时

直到现在我们才接触到props反应哲学的一部分,但忽略了state。下面是一个简单的“有状态”组件:

class App extends Component {
  state = { counter: 0 }

  increment = () => this.setState({
    counter: this.state.counter + 1,
  })

  render = () => (<button onClick={this.increment}>
    {'Counter: ' + this.state.counter}
  </button>)
}

所以,我们有一个counter密钥在我们的状态对象中。单击按钮时会增加其值并更改按钮文本。但是当我们这么做时,在一个Dom里发生了什么?其中哪些部分将重新计算和更新?

呼叫this.setState也会导致重新渲染,但不会导致整个页面,但是只有一个组件本身及其孩子。父母和兄弟姐妹都是免费的。当我们拥有一棵大树时,这很方便,我们只想重绘它的一部分。

钉住问题

我们已经准备好了小演示应用程序所以在我们去修它们之前,你可以看到野外最常见的问题。您可以查看它的源代码。这里。你也需要反应开发工具所以请确保您安装了它们为您的浏览器。

我们首先要看的是哪些元素和何时使虚拟dom被更新。导航到浏览器的dev工具中的响应面板,并选择“突出更新”复选框:

前端优化反应: 虚拟dom解释_第2张图片

在chrome中使用“突出更新”复选框进行响应

现在尝试将一行添加到表中。正如您所看到的,在页面上每个元素周围都出现了边框。这意味着每次添加一行时,响应都是计算和比较整个虚拟dom树。现在尝试在一行中打一个计数器按钮。您可以看到虚拟dom在更改时如何更新state-只有有关因素及其子女受到影响。

对问题可能发生的地方做出反应,但告诉我们细节:尤其是更新问题意味着“diffing”元素或挂载/重新设置它们。为了找到更多的信息,我们需要使用反应的内置探查器(注意它不会在生产模式中工作)。

?react_perf对于您的应用程序的任何url,并进入chrome浏览器中的“性能”选项卡。点击录制按钮并点击桌子周围。添加一些行,更改一些计数器,然后点击“停止”。

前端优化反应: 虚拟dom解释_第3张图片

反应DevTools‘性能’选项卡

在所产生的输出中,我们对“用户计时”感兴趣。缩放到时间线直到看到“反应树协调”组及其孩子。这些都是我们的组件的名称[最新情况][山]在他们旁边。

我们的大部分业绩问题都属于这两类。

无论是组件(以及来自它的所有分支)都是出于某些原因重新安装在每个更新上,我们不希望它(重新安装慢),或者我们正在执行昂贵的和解,大型分支,尽管没有任何改变。

修理东西:安装/安装

现在,当我们发现了一些关于如何做出反应决定更新虚拟dom并了解如何查看幕后发生的事情时,我们终于准备好修复事情了!首先,让我们来处理坐骑/unmounts。

如果您仅仅考虑到任何元素/组件的多个子元素都表示为列阵内部。

考虑到这一点:

/> /> />

在我们的虚拟dom中,它将被表示为:

// ...
props: {
  children: [
    { type: Message },
    { type: Table },
    { type: Footer }
  ]
}
// ...

我们有一个简单的Message这是一个div持有一些文本(想想您的花园品种通知)和一个巨大的Table跨越,比方说,1000+行。他们都是被包围的孩子div所以它们被置于下面props.children在父节点上,它们不会碰巧有一个键。而且反应不会提醒我们通过控制台警告来分配密钥,因为子元素正在被传递给父级React.createElement作为参数列表,而不是数组。

现在我们的用户已经驳回了通知,Message从树上移走。Table以及Footer剩下的都是。

// ...
props: {
  children: [
    { type: Table },
    { type: Footer }
  ]
}
// ...

反应如何看?它把它看作是一个改变形状的儿童的数组:children[0]持有Message现在它占据了Table。没有比与之相比的键,所以比较type因为它们都引用函数(以及异类函数)n.unmounts整体Table然后再挂载它,渲染所有的孩子:1000+行!

所以,您可以添加唯一的键(但是在这个特定的例子中使用键不是最好的选择),或者去寻找一个更聪明的技巧:使用短路布尔估计这是javascript和许多其他现代语言的特点。看:

// Using a boolean trick
<div>
  {isShown && <Message />}
  <Table />
  <Footer />
</div>

即使Message走出画面,props.children父母div仍会持有3元素,children[0]有价值false(布尔基)。记住true/falsenull以及undefined是虚拟dom对象的所有允许值type财产?我们最终得出了这样的结论:

// ...
props: {
  children: [
    false, //  isShown &&  evaluates to false
    { type: Table },
    { type: Footer }
  ]
}
// ...

所以,Message或者不是,我们的索引不会改变,Table当然,将仍然与Table(指组件的引用)type无论如何开始和解,但是仅仅比较虚拟dom比删除Dom节点和从头开始创建它们更快。.

现在让我们来看看更进化的东西。我们知道你喜欢特设s.一个高阶组件是一个函数,它将组件作为参数,做一些事情,并返回一个不同的函数:

function withName(SomeComponent) {
  // Computing name, possibly expensive...
  return function(props) {
    return <SomeComponent {...props} name={name} />;
  }
}

这是一个非常常见的模式,但您需要小心处理它。考虑:

class App extends React.Component() {
  render() {
    // Creates a new instance on each render
    const ComponentWithName = withName(SomeComponent);
    return <SomeComponentWithName />;
  }
}

我们正在创建一个父的内部的一个特殊的render方法。当我们重新渲染树时,我们的虚拟dom看起来就像这样:

// On first render:
{
  type: ComponentWithName,
  props: {},
}

// On second render:
{
  type: ComponentWithName, // Same name, but different instance
  props: {},
}

现在,响应将喜欢只运行一个基于上的算法ComponentWithName但是,正如这个时候,同一个名称引用了不同实例三重相等比较失败,而不是和解,完全重新安装必须发生。注意,它也会导致国家失去正如这里所描述的。幸运的是,它很容易修复:您需要始终在render:

// Creates a new instance just once
const ComponentWithName = withName(Component);

class App extends React.Component() {
  render() {
    return <ComponentWithName />;
  }
}

修复事物:更新

所以,现在我们确保不要重新安装东西,除非必要。然而,对于位于中的树根附近的组件的任何更改都会导致所有子树的重新连接和协调。结构复杂,价格昂贵,而且常常可以避免。

Would be great to have a way to tell React not to look at a certain branch, as we are confident there were no changes in it.

这种方法存在,它涉及到一种称为shouldComponentUpdate它是组件的生命周期。此方法称为以前每个对组件的调用render并接收道具和状态的新值。然后我们可以自由地将它们与当前值进行比较,并决定是否应该更新组件(返回)。truefalse那就是。如果我们返回false反应不会重新渲染所涉组件,并且不会查看其子元素。

通常比较两组props以及state简单的浅层比较是足够的:如果顶级的值不同,我们不必更新。浅比较不是javascript的特性,但却有很多公用事业为了那个。

通过他们的帮助,我们可以编写我们的代码如下:

class TableRow extends React.Component {

  // will return true if new props/state are different from old ones
  shouldComponentUpdate(nextProps, nextState) {
    const { props, state } = this;
    return !shallowequal(props, nextProps)
           && !shallowequal(state, nextState);
  }

  render() { /* ... */ }
}

但是您甚至不必自己编码,因为响应中包含了这个特性,类调用 React.PureComponent。它类似于React.Component只有shouldComponentUpdate已为您实现了浅层道具/状态比较。

听起来好像是个没脑子的,只是交换ComponentPureComponentextends你班的一部分定义并享受效率。不过别这么快!考虑这些例子:

<Table
    // map returns a new instance of array so shallow comparison will fail
    rows={rows.map(/* ... */)}
    // object literal is always "different" from predecessor
    style={ { color: 'red' } }
    // arrow function is a new unnamed thing in the scope, so there will always be a full diffing
    onUpdate={() => { /* ... */ }}
/>

上面的代码片段演示了三个最常见的代码反模式。尽量避开他们!

如果您注意到创建所有对象、数组和函数之外的render定义并确保他们不会在电话之间发生变化-你是安全的。

你可以观察到PureComponent在更新演示所有桌子的位置Rows是“净化”的。如果您在响应DevTools中打开“突出更新”,您将注意到只有表本身和新行正在行插入中呈现,所有其他行都保持不变。

然而,如果你不能等待全力以赴在纯组件上,并在您的应用程序中到处实现它们-停止自己。比较两组props以及state不是免费的,对于大多数基本组件来说都不是值得的:要运行更多的时间。shallowCompare比在算法。

使用这个经验法则:纯组件对复杂表单和表很好,但是它们通常会简化一些简单元素,比如按钮或图标。

关注小编了解更多精彩内容

还可加入我们的前端学习qun:每天收听精品免费学习课堂

同时我将为您分享精品资料,2-1-3-1-2-6-4-8-6 邀请码:落叶


你可能感兴趣的:(前端优化反应: 虚拟dom解释)