React从入门到精通系列之(19)彻底理解React如何重新处理DOM(Diffing算法)

十九、彻底理解React如何重新处理DOM(Diffing算法)

React提供了一个声明式的API,所以你不必担心每次DOM更新时内部会修改哪些东西。虽然在React中并不是那么明显地告诉你具体如何实现的,不过这也让编写应用变得更加容易。

本文会详细解释在React中的“diffing”算法是怎么做的,以便组件更新是可预测的,从而让高性能应用变得足够快。

动机

当使用React时,在单个时间点,您可以将render()函数看做是在创建React元素树。 在下一个stateprops更新时render()函数将返回一个不同的React元素树。 React需要弄清楚如何高效地更新UI去匹配上最新的元素树。

对于将一个树变换成另一个树的最小操作数的算法问题,现在已经存在一些比较通用的解决方案。 然而,那些现有的最先进的技术算法都有O(n^3)的复杂度(n是树中的元素的数量)。

如果在React中使用这些算法,显示1000个元素将需要大约十亿次比较。 这个真的代价太昂贵了。 相反,React实现了一个基于两个假设直观推断出的O(n)算法:

  1. 不同类型的两个元素将产生不同的树。

  2. 开发人员可以在不同渲染之间使用key属性来表示哪些子元素是稳定的。

实际上,这两条假设对几乎所有的实际使用都是有效的。

Diffing算法

当比较两棵DOM树的差异时,React首先比较两个根元素。 如果根元素的类型不同,那么行为也是不同的。

不同类型的DOM元素

每当根元素是不同的类型时,React将删除旧的DOM树并从头开始重新构建新的DOM树。 从<a><img>、从<Article><Comment>、从<Button><div> ,只要不一样就会完全重新构建。

当删除就的DOM树时,旧的DOM节点也被删掉。 这个时候组件实例触发componentWillUnmount()函数 。当构建一个新的DOM树时,新的DOM节点会被插入到DOM中。 组件实例触发componentWillMoun()componentDidMount()。 与之前旧的DOM树相关联的任何state也都将丢失。

在根元素之下的任何组件将被卸载并且它们的state也会全部丢失。 例如:

// 从
<div>
    <Counter />
</div>
// 变为
<span>
    <Counter />
</span>

因为根元素从div变为了span,所以旧的Counter组件将被销毁,然后再重新构建一个新的。

相同类型的DOM元素

当比较相同类型的两个React DOM元素时,React会先查看两者的属性差异,然后保留相同的底层DOM节点,仅仅去更新那些被更改的属性。 例如:

<div className="before" title="hello" />

<div className="after" title="hello" />

通过比较这两个元素属性,React就会知道只需要修改底层DOM节点上的className即可。

当更新style属性时,React也会知道只需要更新style中的那些已更改的属性。 例如:

<div style={{color: 'red',width: '300px'}} />

<div style={{color: 'red',width: '400px'}} />

当在这两个元素之间转换时,React知道只需修改width,而不是color

处理根DOM节点后,React会根据上面的判断逻辑对子节点进行递归扫描。

相同类型的组件元素

当组件更新时,实例保持不变,因此在不同的渲染之间组件内的state是保持不变的。 React会更新底层组件实例的props来匹配新元素,并在底层实例上调用componentWillReceiveProps()componentWillUpdate()

接下来,调用render()方法,diff算法就会对上一个结果和新结果进行递归比较。

递归子元素

默认情况下,当对DOM节点的子元素进行递归时,React只是同时迭代两个子元素lists,并在有差异时产生变化。

例如,当在子元素的末尾再添加一个元素时,这两个树之间就会有一个的很好转换效果:

<ul>
    <l1>one</li>
    <li>two</li>
</ul>

<ul>
    <li>one</li>
    <li>two</li>
    <li>three</li>
</ul>

React将匹配两个<li>one</li>树,匹配两个<li>two</li>树,然后插入一个<li>three</li>树。

但是,不要太天真了。如果在子元素的开头部分插入一个元素的话,性能会便的很差。 例如,这两棵树之间的转换效果就很差:

<ul>
    <li>one</li>
    <li>two</li>
<ul>

<ul>
    <li>zero</li>
    <li>one</li>
    <li>two</li>
<ul>

这种情况React将更改每个子元素 ,而不会意识到它可以保持<li>one</li><li>two</li>子元素树完好无损。 这种低效率的情况是一个必须注意的问题。

keys

为了解决上面的问题,React提供了一个key属性。 当子元素有key属性时,React使用key将原始树中的子元素与后续树中的子元素进行匹配。 例如,上面的那个低效例子添加一个key就可以让子元素树转换变的很有效:

<ul>
    <li key="1">one</li>
    <li key="2">two</li>
<ul>

<ul>
    <li key="0">zero</li>
    <li key="1">one</li>
    <li key="2">two</li>
<ul>

现在React就可以知道key="0"的元素是新的,并且key="1"key="2"的元素只需移动即可。

在实践中,使用一个唯一的key并不难。 您要显示的元素可能已具有唯一的ID,因此key可以来自你自己的数据中:

<li key={item.id>{item.name}</li>

如果不是这样,你可以向数据模型中给每一项数据添加一个新的ID属性,或者对内容的某些部分进行哈希生成keykey属性只有在其兄弟元素之间是唯一的,并不是全局唯一的。

最后一种方式是可以将数组中的索引作为key。 如果数组中的每一项不需要重新排序,同样也可以很好地工作,但是万一需要重新排序的话,这会变的很慢。

权衡利弊

要记住重要的是,diffing算法是一个具体的实现细节。 React可以在每个操作上去重新渲染应用; 最终结果都是一样的。

在当前的实现中,你可以看到一个事实是一个子树已经成功移动到它的兄弟元素当中,但你不能告诉它已经移动到别的地方。 该算法将重新渲染这个完整的子元素树。

因为React很依赖这个直观推断的算法来判断DOM是否需要重新处理,如果不能满足这个算法的那两个假设条件前提,应用的性能将会受到很大影响。

  1. 该算法不会去尝试匹配那些不同组件类型的子元素树。 如果你看到自己在返回相似输出结果的两个组件类型之间来来回回,你可能需要把它们改为相同的类型组件。

  2. key属性应该是稳定,可预测和唯一的。 不稳定的键(如使用Math.random()生的key)将导致许多组件实例和DOM节点进行不必要地重复创建,这可能导致子组件中的性能降低和state丢失。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom