React 中的和解详解

IT技术 reactjs
2021-05-01 16:49:41

我是react JS 的新手。任何人都可以确切地解释和解它是如何工作的。我曾尝试从 react 官方网站上理解它,但没有理解。

2个回答

这就是我的理解:

你会同意 react 使用 components 使事情变得简单和快速。使用 JSX,我们可以让用户定义的组件变得更容易。一天结束时,所有这些都被转换为纯 JavaScript(我假设您了解 React.createElement 的工作原理),函数调用将其他函数调用作为其参数/属性,并保存其他函数调用等等。无论如何,我们没有什么可做的担心react会在内部自行完成。

但这如何为我们提供 UI 呢?为什么它比其他 UI 库更快?

<-- ALL HAIL ReactDOM 库和渲染方法 -->

一个普通的 ReactDOM 调用看起来像这样:

// I have avoided the usage of JSX as its get transpiled anyway 
ReactDOM.render(
  React.createElement(App, { //if any props to pass or child }),    // "creating" a component
  document.getElementById('#root')              // inserting it on a page
);
Heard about VirtualDOM ? { yes : 'Good'} : { no : 'still Good'} ;

React.createElement 根据我们编写的组件构造具有类型和props的元素对象,并将子元素放置在props内的子键下。它以递归方式执行此操作并填充一个最终对象,该对象已准备好转换为等效的 HTML 并绘制到浏览器。

这就是 VirtualDOM,它驻留在 reacts 内存中,react 在这个内存上执行所有操作,而不是在实际的 Browser DOM 上执行。它看起来像这样:

{
  type: 'div',// could be other html'span' or user-diff 'MyComponent'
  props: {
    className: 'cn',
    //other props ...
    children: [
      'Content 1!', // could be a component itself
      'Content 2!', // could be a component itself
      'Content n!', // could be a component itself
    ]
  }
}

构建虚拟 DOM 对象后,ReactDOM.render 会将其转换为我们的浏览器可以根据这些规则绘制 UI 的 DOM 节点:

如果一个类型属性包含一个带有标签名称的字符串——创建一个带有在 props 下列出的所有属性的标签。如果我们在 type 下有一个函数或一个类——调用它并递归地对结果重复这个过程。如果 props 下有任何子节点——对每个子节点一一重复该过程并将结果放置在父节点的 DOM 节点中。

浏览器将其绘制到 UI,这是一项昂贵的任务。React 非常聪明地理解这一点。更新组件意味着创建一个新对象并绘制到 UI。即使涉及一个小的更改,它也会重新创建整个 DOM 树。那么我们如何让浏览器不必每次都创建 DOM 而只绘制必要的东西。

这是我们需要 Reconciliation 和 React 的 diffing 算法的地方 .. 多亏了 React,我们不必自己手动完成它,它在内部得到了照顾,这是一篇很好的文章,可以更深入地理解

现在你甚至可以参考官方的Reconsiliation 文档

值得注意的几点:

React 基于两个假设实现启发式 O(n) 算法:1) 不同类型的两个元素将生成不同的树。2) 开发人员可以通过 key prop 暗示哪些子元素在不同的渲染中可能是稳定的。

在实践中,这些假设几乎适用于所有实际用例。如果不满足这些,则会导致性能问题。

我只是复制粘贴其他几点只是为了了解它是如何完成的:

Diffing :当比较两棵树时,React 首先比较两个根元素。行为因根元素的类型而异。

场景 1:type 是一个字符串,type 在调用中保持不变,props 也没有改变。

// before update
{ type: 'div', props: { className: 'cn' , title : 'stuff'} }

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

这是最简单的情况:DOM 保持不变。

场景二:type还是一样的string,props不同。

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

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

由于 type 仍然代表一个 HTML 元素,React 会查看两者的属性,React 知道如何通过标准 DOM API 调用更改其属性,而无需从 DOM 树中删除底层 DOM 节点。

React 也知道只更新改变的属性。例如:

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

在这两个元素之间转换时,React 知道只修改颜色样式,而不是 fontWeight。

///////当组件更新时,实例保持不变,以便在渲染之间保持状态。React 更新底层组件实例的 props 以匹配新元素,并在底层实例上调用 componentWillReceiveProps() 和 componentWillUpdate()。接下来,render() 方法被调用,diff 算法对先前的结果和新的结果进行递归。处理完 DOM 节点后,React 会在子节点上递归。

场景三:type 变成了不同的 String,或者从 String 变成了一个组件。

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

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

由于 React 现在看到类型不同,它甚至不会尝试更新我们的节点:旧元素将与其所有子元素一起被删除(卸载)。

记住 React 使用 === (triple equals) 来比较类型值很重要,所以它们必须是同一个类或同一个函数的相同实例。

场景4:类型是一个组件。

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

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

“但什么都没有改变!”,你可能会说,那你就错了。

如果 type 是对函数或类(即您的常规 React 组件)的引用,并且我们开始了树协调过程,那么 React 将始终尝试查看组件内部以确保渲染时返回的值没有改变(某种预防副作用的预防措施)。冲洗并重复树下的每个组件 - 是的,复杂的渲染也可能变得昂贵!

为了确保这些事情变得干净:

class App extends React.Component {

  state = {
    change: true
  }

  handleChange = (event) => {
    this.setState({change: !this.state.change})
  }

  render() {
    const { change } = this.state
    return(
      <div>
        <div>
          <button onClick={this.handleChange}>Change</button>
        </div>
        {
          change ? 
          <div>
            This is div cause it's true
            <h2>This is a h2 element in the div</h2>
          </div> :
          <p>
            This is a p element cause it's false
            <br />
            <span>This is another paragraph in the false paragraph</span>
          </p>
        }
      </div>
    )
  }
}

孩子们==============================>

当一个元素有多个子元素时,我们还需要考虑 React 的行为。假设我们有这样一个元素:

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

我们想把这些孩子洗牌:

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

那会发生什么?

如果,在“diffing”时,React 在 props.children 中看到任何数组,它会开始将其中的元素与它之前看到的数组中的元素进行比较,按顺序查看它们:索引 0 将与索引 0 进行比较,索引 1 到索引 1 等。对于每一对,React 将应用上述规则集。

React 有一个内置的方法来解决这个问题。如果元素具有键属性,则元素将通过键的值而不是索引进行比较。只要键是唯一的,React 就会四处移动元素,而不会将它们从 DOM 树中移除,然后将它们放回去(在 React 中称为挂载/卸载的过程)。

所以密钥应该是稳定的、可预测的和唯一的。不稳定的键(如 Math.random() 生成的键)将导致不必要地重新创建许多组件实例和 DOM 节点,这可能导致子组件的性能下降和状态丢失。

因为 React 依赖于启发式,如果不满足它们背后的假设,性能就会受到影响。

当状态改变时: ==========================================>

调用 this.setState 也会导致重新渲染,但不是整个页面的重新渲染,而是组件本身及其子组件的重新渲染。父母和兄弟姐妹幸免于难。当我们有一棵大树并且我们只想重绘它的一部分时,这很方便。

React 上下文中的 Reconciliation 是指让 React 的虚拟 DOM 树与浏览器的真实 DOM 树保持一致。这发生在(重新)渲染期间

关键是不能保证 React 虚拟 DOM 的特定元素在其整个生命周期中引用浏览器的同一个 DOM 节点。这样做的原因是 React 有效更新 DOM 的方法。key如果组件包含动态或有状态的子组件,您可以使用特殊属性来解决此问题。