优化反应: 虚拟dom解释

Posted java落叶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优化反应: 虚拟dom解释相关的知识,希望对你有一定的参考价值。

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

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


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



优化反应: 虚拟dom解释

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



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

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

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

在JSX后面

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

<div className='cn'>
  Content!</div>

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

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

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

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

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

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

  • 基元

    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 请呼叫 <table> 元素及其行作为子。

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

<Table rows={rows} />

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

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

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

将组件放在页面上

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

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

function Table({ rows }) { /* ... */ } 
// defining a component
// rendering a componentReactDOM.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对象中,它们最终都会一起结束。

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

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

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

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

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

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

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

<table>
  <tr>
    <td>Title</td>
  </tr>
  ...</table>

重建在


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

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

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

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

// 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 } }

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

如果 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工具中的响应面板,并选择“突出更新”复选框:



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



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

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

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



反应DevTools‘性能’选项卡



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

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

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

修理东西:安装/安装

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

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

考虑到这一点:

<div>
  <Message />
  <Table />
  <Footer /></div>

在我们的虚拟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 elements, children[0] 有价值 false (布尔基)。记住 true/falsenull 以及 undefined 是虚拟dom对象的所有允许值 type 财产?我们最终得出了这样的结论:

// ...props: {
  children: [
    false, //  isShown && <Message /> 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 onceconst ComponentWithName = withName(Component);
class App extends React.Component() {
  render() {
    return <ComponentWithName />;
  }}

修复事物:更新

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

有办法告诉别人不要看某个分支,这样做是很好的,因为我们相信它没有变化。

这种方法存在,它涉及到一种称为 shouldComponentUpdate 它是 组件的生命周期 。此方法称为 以前 每个对组件的调用 render 并接收道具和状态的新值。然后我们可以自由地将它们与当前值进行比较,并决定是否应该更新组件(返回)。 true false 那就是。如果我们返回 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 已为您实现了 浅层 道具/状态比较。

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

<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={() => { /* ... */ }}/>

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

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

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

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


谢谢你的阅读!现在您已经准备好将这些见解应用到您的应用程序中。您可以使用 仓库 为了我们的小演示( 带着 以及 无 PureComponent 作为你实验的起点。另外,请继续注意本系列的下一部分,我们计划覆盖Redux并优化您的 数据 提高应用程序的总体性能。

DFG_LY 

DFG_LY将为您推送精品阅读



以上是关于优化反应: 虚拟dom解释的主要内容,如果未能解决你的问题,请参考以下文章

第1273期React性能优化-虚拟Dom原理浅析

“反应堆模式”及其应用的简单解释[关闭]

虚拟DOM -------- 最易理解的解释

虚拟DOM性能优化实战,同样是操作DOM,为什么说他快?

2.ReactJS基础(虚拟DOM,JSX语法)

react性能优化