接下来用vdom(Virtual DOM)来简称为虚拟DOM。css
指的是用JS模拟的DOM结构,将DOM变化的对比放在JS层来作。换而言之,虚拟DOM就是JS对象。
以下DOM结构:
<ul id="list"> <li class="item">Item1</li> <li class="item">Item2</li> </ul>
映射成虚拟DOM就是这样:html
{ tag: "ul", attrs: { id: "list" }, children: [ { tag: "li", attrs: { className: "item" }, children: ["Item1"] }, { tag: "li", attrs: { className: "item" }, children: ["Item2"] } ] }
React会去调用render()方法来从新渲染整个组件的UI,可是若是咱们真的去操做这么大量的DOM,显然性能是堪忧的。因此React实现了一个Virtual DOM,组件的真实DOM结构和Virtual DOM之间有一个映射的关系,React在虚拟DOM上实现了一个diff算法,当render()去从新渲染组件的时候,diff会找到须要变动的DOM,而后再把修改更新到浏览器上面的真实DOM上,因此,React并非渲染了整个DOM树,Virtual DOM就是JS数据结构,因此 理论上原生的DOM快得多。react
现有一个例子:jquery
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>eg</title> </head> <body> <div id="box"></div> <button id="btn">点击</button> <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script> <script> const data = ['哈哈', '呵呵','嘿嘿'] //渲染函数 function render(data) { const $box = $('#box'); $box.html(''); const $ul = $('<ul>'); // 重绘一次 $ul.append($('<li>10</li>')); data.forEach(item => { //每次进入都重绘 $ul.append($(`<li>${item}</li>`)) }) $box.append($ul); } $('#btn').click(function () { data[1] = data[1] + '嘿'; render(data); }); render(data) </script> </body> </html>
①用JS对象构建一颗虚拟DOM树,而后用虚拟树构建一颗真实的DOM树,而后插入到文档中。
②当状态变动时,从新构造一颗新的对象树,而后新树旧树进行比较,记录两树差别。
③把步骤2的差别应用到步骤1所构建的真实DOM树上,视图就更新了。算法
虚拟DOM的优势还有:编程
一、函数式的UI编程,即UI = f(data)这种构建UI的模式。浏览器
二、能够将JS对象渲染到浏览器DOM之外的环境中,也就是支持了跨平台开发,好比ReactNative。数据结构
React的 virtual dom的性能好也离不开它自己特殊的diff算法。传统的diff算法时间复杂度达到o(n3),而react的diff算法时间复杂度只是o(n),react的diff能减小到o(n)依靠的是react diff的三大策略。app
传统diff 对比 react diff
传统的diff算法追求的是“彻底”以及“最小”,而react diff则是放弃了这两种追求:
在传统的diff算法下,对比先后两个节点,若是发现节点改变了,会继续去比较节点的子节点,一层一层去对比。就这样循环递归去进行对比,复杂度就达到了o(n3),n是树的节点数,想象一下若是这棵树有1000个节点,咱们得执行上十亿次比较,这种量级的对比次数,时间基本要用秒来作计数单位了。那么react到底是如何把复杂度下降到o(n)的呢?dom
React diff 三大策略
策略一(tree diff):Web UI中DOM节点跨层级的移动操做特别少,能够忽略不计。(DOM结构发生改变-----直接卸载并从新creat)
策略二(component diff):DOM结构同样-----不会卸载,可是会update
策略三(element diff):全部同一层级的子节点.他们均可以经过key来区分-----同时遵循1.2两点
虚拟DOM树分层比较(tree diff)
两棵树只会对同一层次的节点进行比较,忽略DOM节点跨层级的移动操做。React只会对相同颜色方框内的DOM节点进行比较,即同一个父节点下的全部子节点。当发现节点已经不存在,则该节点及其子节点会被彻底删除掉,不会用于进一步的比较。这样只须要对树进行一次遍历,便能完成整个DOM树的比较。由此一来,最直接的提高就是复杂度变为线型增加而不是原先的指数增加。
可是若是DOM节点出现了跨层级操做,diff会如何处理?
就好比上图,A节点及其子节点进行移动挂到另外一个DOM下时,React是不会机智的判断出子树仅仅是发生了移动,而是会直接销毁,并从新建立这个子树,而后再挂在到目标DOM上。实际上,React官方也并不推荐咱们作出跨层级的骚操做。因此咱们能够从中悟出一个道理:就是咱们本身在实现组件的时候,一个稳定的DOM结构是有助于咱们的性能提高的。
组件间的比较(component diff)
查阅的网上的不少资料,发现写的都比较难懂,根据我本身的理解,其实最核心的策略仍是看结构是否发生改变。React是基于组件构建应用的,对于组件间的比较所采用的策略也是很是简洁和高效的。
若是是同一个类型的组件,则按照原策略进行Virtual DOM比较。
若是不是同一类型的组件,则将其判断为dirty component,从而替换整个组价下的全部子节点。
若是是同一个类型的组件,有可能通过一轮Virtual DOM比较下来,并无发生变化。若是咱们可以提早确切知道这一点,那么就能够省下大量的diff运算时间。所以,React容许用户经过shouldComponentUpdate()来判断该组件是否须要进行diff算法分析。
如上图所示,当组件D变为组件G时,哪怕这两个组件结构类似,一旦React判断D和G是不用类型的组件,就不会比较二者的结构,而是直接删除组件D,从新建立组件G及其子节点。也就是说,若是当两个组件是不一样类型但结构类似时,其实进行diff算法分析会影响性能,可是毕竟不一样类型的组件存在类似DOM树的状况在实际开发过程当中不多出现,所以这种极端因素很难在实际开发过程当中形成重大影响。
元素间的比较(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进行标识。
相信大部分刚开始接触react的时候,都看到过这样的警告:
这是因为咱们在循环渲染列表时候(map)时候忘记标记key值报的警告,既然是警告,就说明即便没有key的状况下也不会影响程序执行的正确性.其实这个key的存在与否只会影响diff算法的复杂度,也就是说你不加上Key就会像上面的例子同样暴力渲染,加了Key以后,React就能够作出移动的操做了,看例子:
和上面的例子是同样的,只不过每一个节点都加上了惟一的key值,经过这个Key值发现新老集合里面其实所有都是相同的元素,只不过位置发生了改变。所以就无需进行节点的建立、插入、删除等操做了,只须要将老集合当中节点的位置进行移动就能够了。React给出的diff结果为:B、D不作操做,A、C进行移动操做。react是如何判断谁该移动,谁该不动的呢?
react会去循环整个新的集合:
①重新集合中取到B,而后去旧集合中判断是否存在相同的B,确认B存在后,再去判断是否要移动:
B在旧集合中的index = 1,有一个游标叫作lastindex。默认lastindex = 0,而后会把旧集合的index和游标做对比来判断是否须要移动,若是index < lastindex ,那么就作移动操做,在这里B的index = 1,不知足于 index < lastindex,因此就不作移动操做,而后游标lastindex更新,取(index, lastindex) 的较大值,这里就是lastindex = 1
②而后遍历到A,A在老集合中的index = 0,此时的游标lastindex = 1,知足index < lastindex,因此对A须要移动到对应的位置,此时lastindex = max(index, lastindex) = 1
③而后遍历到D,D在老集合中的index = 3,此时游标lastindex = 1,不知足index < lastindex,因此D保持不动。lastindex = max(index, lastindex) = 3
④而后遍历到C,C在老集合中的index = 2,此时游标lastindex = 3,知足 index < lastindex,因此C移动到对应位置。C以后没有节点了,diff就结束了
以上主要分析新老集合中节点相同但位置不一样的情景,仅对节点进行位置移动的状况,若是新集合中有新加入的节点且老集合存在须要删除的节点,那么 React diff 又是如何对比运做的呢?
和第一种情景基本是一致的,react仍是去循环整个新的集合:
①不赘述了,和上面的第一步是同样的,B不作移动,lastindex = 1
②新集合取得E,发现旧集合中不存在,则建立E并放在新集合对应的位置,lastindex = 1
③遍历到C,不知足index < lastindex,C不动,lastindex = 2
④遍历到A,知足index < lastindex,A移动到对应位置,lastindex = 2
⑤当完成新集合中全部节点 diff 时,最后还须要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,所以删除节点 D,到此 diff 所有完成
可是 react diff也存在一些问题,和须要优化的地方,看下面的例子:
在上面的这个例子,A、B、C、D都没有变化,仅仅是D的位置发生了改变。看上面的图咱们就知道react并无把D的位置移动到头部,而是把 A、B、C分别移动到D的后面了,经过前面的两个例子,咱们也大概知道,为何会发生这样的状况了:
由于D节点在老集合里面的index 是最大的,使得A、B、C三个节点都会 index < lastindex,从而致使A、B、C都会去作移动操做。因此在开发过程当中,尽可能减小相似将最后一个节点移动到列表首部的操做,当节点数量过大或更新操做过于频繁时,在必定程度上会影响 React 的渲染性能。
因此通过这么一分析react diff的三大策略,咱们可以在开发中更加进一步的提升react的渲染效率。
箴言一:在开发组件时,保持稳定的 DOM 结构会有助于性能的提高;
箴言二:使用 shouldComponentUpdate()方法节省diff的开销
箴言三:在开发过程当中,尽可能减小相似将最后一个节点移动到列表首部的操做,当节点数量过大或更新操做过于频繁时,在必定程度上会影响 React 的渲染性能。
咱们再写react的时候,当咱们作map循环的时候,当咱们没有一个惟一id来标识每一项item的时候,咱们可能会选择使用index,官网不推荐咱们使用index做为key,经过上面的知识背景,咱们其实能够知道为何使用index会致使一些问题:
看下面的一个场景吧:
data.map((item, index) => { return <li key={index}>{item}</li> })
上面这样写会存在很大的坑,好比看下面的例子:
class App extends 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 ( <Li key={index} val={item.val}></Li> ) }) } <button onClick={this.click.bind(this)}>Reverse</button> </ul> ) } } class Li extends Component{ constructor(props) { super(props) } componentDidMount() { console.log('===mount===') } componentWillUpdate() { console.log('===update====') } render() { return ( <li> {this.props.val} <input type="text"></input> </li> ) } }
咱们在三个输入框里面,依次输入1,2,3,点击Reverse按钮,按照咱们的预期,这时候页面应该渲染成3,2,1,可是实际上,顺序依然仍是1,2,3,再看控制台里面,确实是打印了===update===
,证实数据确实是更新了的。那么为何会发生这种事情,咱们能够分析一下:
咱们能够看下这个图就明白了:
就像咱们以前所说,react会经过key去老集合中找,是否有相同的元素,react发现新老key都是一致的,他会认为是同一个组件,因此input框内的值没有倒叙。咱们只须要乖乖的把做为,就能够解决这个现象了。
还有存在一点隐藏的(性能问题):
当咱们对数据有 删除、添加 等操做时。咱们所遍历的index,就会有所变化,这种状况下diff算法对新老集合进行差别检测,发现key值有变化而后就会从新渲染,
咱们只须要乖乖的把做为,这样就只会对key值有变化的进行重绘,就能够解决这种性能问题了。idkeyid(或者其余惟一标识)key