浏览器性能优化(2)React 虚拟 dom与diff算法

随着前端技术快速发展,现在的mvvm几大框架遍布前端行业,那么它们对浏览器的性能到底影响多大?与传统的jq相比做了哪些优化呢?

文章目录:

  • React中的虚拟DOM是什么?
  • 虚拟DOM的简单实现(diff算法)
  • React 进行DOM操作时,注意三点
  • 为何不推荐将index作为key?
React中的虚拟DOM是什么?

React的核心思想:一个Component拯救世界,忘掉烦恼,从此不再操心界面
  
1. Virtual Dom渲染

1.1 Javascript很快
   Chrome刚出来的时候,在Chrome里跑Javascript非常快,给了其它浏览器很大压力。而现在经过几轮你追我赶,各主流浏览器的Javascript执行速度都很快了。单一的javascript其实是很快的,主流的浏览器已经处理的比较好
1.2 DOM操作很慢
   当创建一个元素比如div,有以下几项内容需要实现: HTML element、Element、GlobalEventHandler。简单的说,就是插入一个Dom元素的时候,这个元素上本身或者继承很多属性如 width、height、offsetHeight、style、title,另外还需要注册这个元素的诸多方法,比如onfucos、onclick等等。 这还只是一个元素,如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率很低。
  比如,我们在一个空白网页的body中添加一个div元素,如下所示: 在这里插入图片描述
   这个元素会挂载默认的styles、得到这个元素的computed属性、注册相应的Event Listener、DOM Breakpoints以及大量的properties,这些属性、方法的注册肯定是需要耗费大量时间的。
   尤其是在js操作DOM的过程中,不仅有dom本身的繁重,js的操作也需要浪费时间,我们认为js和DOM之间有一座桥,如果你频繁的在桥两边走动,显然效率是很低的,如果你的JavaScript操作DOM的方式还非常不合理,那么显然就会更糟糕了。
  而 React的虚拟DOM就是解决这个问题的! 虽然它解决不了DOM自身的繁重,但是虚拟DOM可以对JavaScript操作DOM这一部分内容进行优化。
 
比如list是这样的

<ul>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

现在想要变成这样

<ul>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>

通常的操作是什么?
  先把0, 1,2,3这些Element删掉,然后加几个新的Element 6,7,8,9,10进去,这里面就有4次Element删除,5次Element添加。共计9次DOM操作
  那React的虚拟DOM可以怎么做呢?
  而React会把这两个做一下Diff,然后发现其实不用删除0,1,2,3,而是可以直接改innerHTML,然后只需要添加一个Element(10)就行了,这样就是4次innerHTML操作加1个Element添加。共计5次操作,这样效率的提升是非常可观的。
  
2. 关于React
2.1 接口和设计
  在React的设计中,是完全不需要你来操作DOM的。我们也可以认为,在React中根本就没有DOM这个概念,有的只是Component。
  当你写好一个Component以后,Component会完全负责UI,你不需要也不应该去也不能够指挥Component怎么显示,你只能告诉它你想要显示一个香蕉还是两个梨。
  隔离DOM并不仅仅是因为DOM慢,而也是为了把界面和业务完全隔离,操作数据的只关心数据,操作界面的只关心界面。比如在websocket聊天室的创建房间时,我们可以首先Component写好,然后当获取到数据的时候,只要把数据放在redux中就好,然后Component就动把房间添加到页面中去,而不是你先拿到数据,然后使用js操作DOM把数据显示在页面上。
  即我提供一个Component,然后你只管给我数据,界面的事情完全不用你操心,我保证会把界面变成你想要的样子。所以说React的着力点就在于View层,即React专注于View层。你可以把一个React的Component想象成一个Pure Function,只要你给的数据是[1, 2, 3],我保证显示的是[1, 2, 3]。没有什么删除一个Element,添加一个Element这样的事情。NO。你要我显示什么就给我一个完整的列表。
  另外,Flux虽然说的是单向的Data Flow(redux也是),但是实际上就是单向的Observer,Store->View->Action->Store(箭头是数据流向,实现上可以理解为View监听Store,View直接trigger action,然后Store监听Action)。
  这就是MVVM框架,MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型。【模型】指的是后端传递的数据。【视图】指的是所看到的页面。【视图模型】mvvm模式的核心,它是连接view和model的桥梁。它有两个方向:一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,
即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。

2.2 实现
  那么react如何实现呢? 最简单的方法就是当数据变化时,我直接把原先的DOM卸载,然后把最新数据的DOM替换上去。 但是,虚拟DOM哪去了? 这样做的效率显然是极低的
  所以虚拟DOM就来救场了。
  那么虚拟DOM和DOM之间的关系是什么呢?
  首先,Virtual DOM并没有完全实现DOM,即虚拟DOM和真正地DOM是不一样的,Virtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性。因为真实DOM实在是太复杂,一个空的Element都复杂得能让你崩溃,并且几乎所有内容我根本不关心好吗。所以Virtual DOM里每一个Element实际上只有几个属性,即最重要的,最为有用的,并且没有那么多乱七八糟的引用,比如一些注册的属性和函数啊,这些都是默认的,创建虚拟DOM进行diff的过程中大家都一致,是不需要进行比对的。所以哪怕是直接把Virtual DOM删了,根据新传进来的数据重新创建一个新的Virtual DOM出来都非常非常非常快。(每一个component的render函数就是在做这个事情,给新的virtual dom提供input)。
   所以,引入了Virtual DOM之后,React是这么干的:你给我一个数据,我根据这个数据生成一个全新的Virtual DOM,然后跟我上一次生成的Virtual DOM去 diff,得到一个Patch,然后把这个Patch打到浏览器的DOM上去。完事。并且这里的patch显然不是完整的虚拟DOM,而是新的虚拟DOM和上一次的虚拟DOM经过diff后的差异化的部分
  最后,回到为什么Virtual Dom快这个问题上。
其实是由于每次生成virtual dom很快,diff生成patch也比较快,而在对DOM进行patch的时候,虽然DOM的变更比较慢,但是React能够根据Patch的内容,优化一部分DOM操作,比如之前的那个例子。
  重点就在最后,哪怕是我生成了virtual dom(需要耗费时间),哪怕是我跑了diff(还需要花时间),但是我根据patch简化了那些DOM操作省下来的时间依然很可观(这个就是时间差的问题了,即节省下来的时间 > 生成 virtual dom的时间 + diff时间)。所以总体上来说,还是比较快。

此部分内容整理参考https://www.cnblogs.com/zhuzhenwei918/p/7271305.html

虚拟DOM的简单实现(diff算法)

1. 什么是虚拟DOM
  当我们更新了state中的值的时候,React会去调用render()方法来重新渲染整个组件的UI,但是如果我们真的去操作这么大量的DOM,显然性能是堪忧的。所以React实现了一个Virtual DOM,组件的真实DOM结构和Virtual DOM之间有一个映射的关系,React在虚拟DOM上实现了一个diff算法,当render()去重新渲染组件的时候,diff会找到需要变更的DOM,然后再把修改更新到浏览器上面的真实DOM上,所以,React并不是渲染了整个DOM树 ,Virtual DOM就是JS数据结构,所以比原生的DOM快得多。
2. 操作原生DOM慢?虚拟DOM快?
  DOM全称文档对象模型,本质也是一个JS对象,所以操作DOM对象其实也是很快的,慢就慢在操作了DOM对象之后,浏览器之后所做出的行为,比如布局layout、绘制paint。比如原生代码中,我们需要操作10个DOM,我们的理想状态是一次性构建出全部的DOM树,然后渲染,但是实际上,浏览器收到第一个DOM操作请求之后,它并不知道你后面还有9个操作,它就会走一遍完整的渲染流程,显然像计算元素坐标这些操作都是白白浪费的,因为下一次DOM操作可能会改变这些坐标,前面的计算就白费了。
3. 原生DOM优化
  我们想要去优化原生的DOM操作,其实是去试图减少layout的次数,我们唯一能想到可以减少layout次数的办法就是缓存我们的DOM操作,建议用一个变量将DOM节点保存起来,当遍历Dom操作后再一次性渲染。
4. 再次强调,为何会存在vertual dom?
  当然是因为方便了,因为这些优化,react内部是默认实施的,并且性能很优,不用我们每次都自己写一大堆缓存操作。框架的存在意义也就在于便于程序员开发嘛。并且这样,我们也更容易去维护我们的代码了。我们就可以欢快的敲代码了。
  virtual dom比原生dom快,就它的batching 和独特的 diff算法,batching就是把所有的DOM操作搜集起来,一次性提交给真实DOM,diff算法通过对比新旧虚拟DOM树,记录之间的差异。

接下来进入正题啦,vertual dom操作, diff算法

5. 虚拟DOM流程

  1. 用JS对象构建一颗虚拟DOM树,然后用虚拟树构建一颗真实的DOM树,然后插入到文档中。
  2. 当状态变更时,重新构造一颗新的对象树,然后新树旧树进行比较,记录两树差异。
  3. 把差异化内容渲染到视图, 视图就更新了。

6. diff算法
  其实React的 virtual dom的性能也离不开它本身特殊的diff算法。传统的diff算法时间复杂度达到o(n3),而react的diff算法时间复杂度只是o(n),react的diff能减少到o(n)依靠的是react diff的三大策略。
  
7.传统diff 对比 react diff
  传统的diff算法追求的是“完全”以及“最小”,而react diff则是放弃了这两种追求:
  在传统的diff算法下,对比前后两个节点,如果发现节点改变了,会继续去比较节点的子节点,一层一层去对比。就这样循环递归去进行对比,复杂度就达到了o(n3),n是树的节点数,想象一下如果这棵树有1000个节点,我们得执行上十亿次比较,这种量级的对比次数,时间基本要用秒来做计数单位了。那么react究竟是如何把复杂度降低到o(n)的呢?

8.React diff 三大策略

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。(tree diff)
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结(component diff)
  3. 所有同一层级的子节点.他们都可以通过key来区分-----同时遵循1.2两点(element diff)

8.1 虚拟DOM树分层比较(tree diff)
8.1.1 同层比较,diff会如何处理呢?
   两棵树只会对同一层次的节点进行比较忽略DOM节点跨层级的移动操作。React只会对相同颜色方框内的DOM节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。由此一来,最直接的提升就是复杂度变为线型增长而不是原先的指数增长。
在这里插入图片描述
8.1.2 跨层比较,diff会如何处理呢?
在这里插入图片描述
就比如上图,B节点及其子节点进行移动挂到另一个DOM下时,React是不会机智的判断出子树仅仅是发生了移动,而是会直接销毁,并重新创建这个子树,然后再挂在到目标DOM上。实际上,React官方也并不推荐我们做出跨层级的骚操作。所以我们可以从中悟出一个道理:就是我们自己在实现组件的时候,一个稳定的DOM结构是有助于我们的性能提升的
9. 组件间的比较(component diff)
  查阅的网上的很多资料,发现写的都比较难懂,根据我自己的理解,其实最核心的策略还是看结构是否发生改变。React是基于组件构建应用的,对于组件间的比较, 所采用的策略也是非常简洁和高效的。
  如果是同一个类型的组件,则按照原策略进行Virtual DOM比较。
  如果不是同一类型的组件,则将其判断为dirty component,从而替换整个组价下的所有子节点。
  如果是同一个类型的组件,有可能经过一轮Virtual DOM比较下来,并没有发生变化。如果我们能够提前确切知道这一点,那么就可以省下大量的diff运算时间。因此,React允许用户通过shouldComponentUpdate()来判断该组件是否需要进行diff算法分析。
在这里插入图片描述
  如上图所示,当组件C变为组件H时,哪怕这两个组件结构相似,一旦React判断C和H是不同类型的组件,就不会比较两者的结构,而是直接删除组件C,重新创建组件H及其子节点。也就是说,如果当两个组件是不同类型但结构相似时,其实进行diff算法分析会影响性能,但是毕竟不同类型的组件存在相似DOM树的情况在实际开发过程中很少出现,因此这种极端因素很难在实际开发过程中造成重大影响。
10.元素间的比较(element diff)
  当节点处于同一层级的时候,react diff 提供了三种节点操作:插入、删除、移动。

操作描述
插入新节点在老集合当中不存在,即全新的节点,就会执行插入操作
移动新节点在老集合中存在,并且只做了位置上的更新,就会复用之前的节点,做移动操作(依赖于Key)
删除新节点在老集合中存在,但节点做出了更改不能直接复用,做出删除操作
先来看个简单的例子

在这里插入图片描述
  看上面的例子,得知,老集合包含节点 A、B、C、D,更新之后的新集合包括节点: B、A、D、C,然后diff算法对新老集合进行差异检测,发现B不等于A,然后就会创建B然后插入,并删除A节点,以此类推,创建并插入 A、D、C,然后移除B、C、D。
但是这些节点其实都没有发生改变,仅仅是位置上发生了变化,却要进行一大堆的繁琐低效的创建插入删除等操作,React说:“这样下去不行的,我们不如。。。”,于是React允许开发者对同一层级的同组子节点增加一个唯一的Key进行标识

key的作用

相信很多人在接触React后,都会看到控制台经常有这样的警告。
在这里插入图片描述
  这是由于我们在循环渲染列表时候(map)时候忘记标记key值报的警告,既然是警告,就说明即使没有key的情况下也不会影响程序执行的正确性.其实这个key的存在与否只会影响diff算法的复杂度,也就是说你不加上Key就会像上面的例子一样暴力渲染,加了Key之后,React就可以做出移动的操作了,看例子:
在这里插入图片描述
  和上面的例子是一样的,只不过每个节点都加上了唯一的key值,**通过这个Key值发现新老集合里面其实全部都是相同的元素,只不过位置发生了改变。**因此就无需进行节点的创建、插入、删除等操作了,只需要将老集合当中节点的位置进行移动就可以了。
  React给出的diff结果为:B、D不做操作,A、C进行移动操作。react是如何判断谁该移动,谁该不动的呢?

React源码逻辑输理:

react会去循环整个新的集合:

  1. 从新集合中取到B,然后去旧集合中判断是否存在相同的B(根据唯一的key来判断),确认B存在后,再去判断是否要移动:
  2. 在旧集合中的index = 1,有一个游标叫做lastindex。默认lastindex = 0,然后会把旧集合的index和游标作对比来判断是否需要移动,如果index < lastindex ,那么就做移动操作,在这里B的index = 1,不满足于 index < lastindex,所以就不做移动操作,然后游标lastindex更新,取(index, lastindex) 的较大值,这里就是lastindex = 1
  3. 然后遍历到A,A在老集合中的index = 0,此时的游标lastindex = 1,满足index < lastindex,所以对A需要移动到对应的位置,此时lastindex = max(index, lastindex) = 1
  4. 然后遍历到D,D在老集合中的index = 3,此时游标lastindex = 1,不满足index < lastindex,所以D保持不动。lastindex = max(index, lastindex) = 3
  5. 然后遍历到C,C在老集合中的index = 2,此时游标lastindex = 3,满足 index < lastindex,所以C移动到对应位置。C之后没有节点了,diff就结束了
将上面的原理整理成一张表,如下:

此种规则适用于节点相同位置不同的情况,仅适用于移动位置

在这里插入图片描述

如果新集合中有新加入的节点且老集合存在需要删除的节点,那么 React diff 又是如何对比运作的呢?

在这里插入图片描述

和第一种情景基本是一致的,react还是去循环整个新的集合:

  1. 不赘述了,和上面的第一步是一样的,B不做移动,lastindex = 1
  2. 新集合E,发现旧集合中不存在,则创建E并放在新集合对应的位置,lastindex = 1
  3. 遍历到C,不满足index < lastindex,C不动,lastindex = 2
  4. 遍历到A,满足index < lastindex,A移动到对应位置,lastindex = 2
  5. 完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成
针对新集合存在新节点的插入,老集合存在删除的节点

在这里插入图片描述

但react diff也存在一定的问题,看下面例子

在这里插入图片描述

在上面的这个例子,A、B、C、D都没有变化,仅仅是D的位置发生了改变。看上面的图我们就知道react并没有把D的位置移动到头部,而是把 A、B、C分别移动到D的后面了,通过前面的两个例子(react针对移动的解析原理来看),我们也大概知道,为什么会发生这样的情况了:
  因为D节点在老集合里面的index 是最大的,使得A、B、C三个节点都会 index < lastindex,从而导致A、B、C都会去做移动操作。所以在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

React 进行DOM操作时,注意三点:

经过这么一分析react diff的三大策略,我们能够在开发中更加进一步的提高react的渲染效率。

  1. 在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
  2. 使用 shouldComponentUpdate()方法节省diff的开销
  3. 在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

为何不推荐将index作为key?
  我们再写react的时候,当我们做map循环的时候,当我们没有一个唯一id来标识每一项item的时候,我们可能会选择使用index,官网不推荐我们使用index作为key,通过上面的知识背景,我们其实可以知道为什么使用index会导致一些问题:
  在这里插入图片描述
  像这样是有问题的,我们来看一个例子

import React from 'react';
import { connect } from 'dva';
import {Button} from 'antd';
// import 'antd/lib/button/style';
import Mypage from './mypage'
class IndexPage extends React.Component {
  constructor(props) {
		super(props)
		this.state = {
			list: [{id: 1,val: 'A'}, {id: 2, val: 'B'}, {id: 3, val: 'C'}]
		}
	}

	click() {
		this.state.list.reverse()
		this.setState({})
	}
  render() {
    return (
      <ul>
          {
            this.state.list.map((item, index) => {
                  return (
                      <Mypage key={index} val={item.val}></Mypage>
                  )
              })
          }
           <button onClick={this.click.bind(this)}>Reverse</button>
      </ul>
)  }

}

IndexPage.propTypes = {
};

export default connect()(IndexPage);

import React from 'react';
import { connect } from 'dva';
// import styles from './IndexPage.css'
class Mypage extends React.Component {
  constructor(props) {
		super(props)
	}
	componentDidMount() {
		console.log('===mount===')
	}
	componentWillUpdate() {
		console.log('===update====')
	}
	render() {
		return (
            <li>
                {this.props.val}
                <input type="text"></input>
            </li>
		)
	}
}

Mypage.propTypes = {
};

export default connect()(Mypage);



在这里插入图片描述
根据运行结果来看,当输入1 2 3,按理应该返回3 2 1 ,而实际上还是返回1 2 3 ,且控制台已经可以看到update,说明数据确实已经更新了的,那这是什么问题呢?**就是因为index作为 key,**来看例子:
在这里插入图片描述
就像我们之前所说,react会通过key去老集合中找,是否有相同的元素,react发现新老key都是一致的,他会认为是同一个组件,所以input框内的值没有倒叙。我们只需要乖乖的把id作为key,就可以解决这个现象了

该内容都是本人的一些理解,本文如有不足之处,还请友友们及时提出。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值