react——diff

DOM的Diffing算法

什么是虚拟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快得多。

virtual dom 基本步骤

①用JS对象构建一颗虚拟DOM树,然后用虚拟树构建一颗真实的DOM树,然后插入到文档中。
②当状态变更时,重新构造一颗新的对象树,然后新树旧树进行比较,记录两树差异。
③把步骤2的差异应用到步骤1所构建的真实DOM树上,视图就更新了。

diff算法

其实React的 virtual dom的性能好也离不开它本身特殊的diff算法。传统的diff算法时间复杂度达到o(n3),而react的diff算法时间复杂度只是o(n),react的diff能减少到o(n)依靠的是react diff的三大策略。

传统diff 对比 react diff

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

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树的比较。由此一来,最直接的提升就是复杂度变为线型增长而不是原先的指数增长。
Diff

组件间的比较(component diff)

如果是同一个类型的组件,则按照原策略进行Virtual DOM比较。
如果不是同一类型的组件,则将其判断为dirty component,从而替换整个组价下的所有子节点。
如果是同一个类型的组件,有可能经过一轮Virtual DOM比较下来,并没有发生变化。如果我们能够提前确切知道这一点,那么就可以省下大量的diff运算时间。因此,React允许用户通过shouldComponentUpdate()来判断该组件是否需要进行diff算法分析。

组件对比

如上图所示,当组件D变为组件G时,哪怕这两个组件结构相似,一旦React判断D和G是不用类型的组件,就不会比较两者的结构,而是直接删除组件D,重新创建组件G及其子节点。也就是说,如果当两个组件是不同类型但结构相似时,其实进行diff算法分析会影响性能,但是毕竟不同类型的组件存在相似DOM树的情况在实际开发过程中很少出现,因此这种极端因素很难在实际开发过程中造成重大影响。

元素间的比较(element diff)

当节点处于同一层级的时候,react diff 提供了三种节点操作:插入、删除、移动。

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

经典面试题:

1.react/vue中的key有什么作用?(key的内部原理是什么)

2.为什么遍历列表时,key最好不用index

1.虚拟DOM中key的作用:

  1. 简单的说:key是虚拟DOM对象的标识,在更新显示时,key起着至关重要的作用
  2. 详细的说:当状态中的数据发生变化时,react会根据新数据生成新的虚拟DOM,随后react进行新虚拟DOM和旧虚拟DOM的diff对比,比较规则如下
    1. 旧虚拟 DOM中找到了与新虚拟DOM相同的key:
      1. 若虚拟DOM中内容没有变化,直接使用之前的真实DOM
      2. 若虚拟DOM中的内容变了,则生成真实DOM,随后替换掉页面之前的真实DOM
    2. 旧虚拟DOM中未找到与新虚拟DOM相同的key
      1. 根据数据创建新的真实DOM,随后渲染到页面

2.用index作为key可能引发的问题

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:会产生没有必要的真实DOM更新 ==> 页面没有问题,但是效率会很低
  2. 如果结构中还包含输入类DOM:会产生错误DOM == > 页面有问题

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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>
)
}
}