dom 是浏览器的概念,也是浏览器独有的, 虚拟dom是react框架的概念
React的高效得益于其Virtual DOM 加 React diff的体系
DOM(Document Object Model)
它是浏览器提供的概念,用js对象来表示页面上的元素,并提供了一些操作DOM对象的API
例如: document.querySelector, getElementById等
Virtual DOM(本质是js对象)
它是框架中的概念,是程序员通过js对象来模拟页面上的DOM元素和DOM元素嵌套关系的
<div id="xx" title="你看">明明很帅<p>诺诺最美</p></div>用js对象来模拟dom元素var div = {tagName: 'div',attr: {id: 'xx',title: '你看'},children: ['明明很帅',{tagName: 'p',attr: {},children: ['诺诺最美']}]}可以通过js对象来模拟新旧dom树,来比较更新的dom节点,为的就是实现页面元素的高效更新当我们实际开发使用React的时候,在某个时间点 render() 函数创建了一棵React元素树,也就模拟一个虚拟 DOM 树在下一个state或者props更新的时候,render() 函数将创建一棵新的React元素树, 也就模拟了一个新的虚拟 DOM 树既然模拟出了新旧两棵DOM 树, 那么如何高效的进行新旧两棵树的对比呢??当然是使用 DIff 算法...
为什么要实现 Virtual DOM(Virtual DOM 目的)
为了实现页面中,DOM 元素的高效更新
diff 算法
虚拟DOM 可以实现页面元素高效更新,就要进行新旧dom树的对比,新旧dom树对比的过程高效,才能真正实现高效更新。那怎么实现高效的对比呢?下面有三个概念:
- tree diff : 新旧两棵dom树,逐层对比的过程就是 tree diff, 当整棵DOM树逐层对比完毕,则所有需要被按需更新的元素,必然能够被找到。
- component diff : 在进行tree diff的时候,每一层中,都有自己的组件,组件级别的对比,叫做 compoent diff。如果对比前后,组件的类型相同,则暂时认为此组件不需要更新;如果对比前后,组件的类型不同,则需要移除旧组件,创建新组件,并渲染到页面上。
- element diff :在进行组件对比的时候,如果两个组件类型相同,则需要进行元素级别的对比,这叫做element diff。

react 中为何推荐设置 key
最初接触react的时候,只知道map返回的元素,要设置key属性,不设置key属性的话,控制台会提示如下warning
,却始终不知为何。我相信初次使用react的小伙伴都会有此疑惑,那我举例说下我的见解。
不设置key属性的情况:
//react代码ReactDom.render(<div>{[1, 2].map(item => <span>{item}</span>)}</div>,document.getElementById('#root'));//首先经过babel编译成如下代码,jsx ---> jsReactDom.render(React.createElement("div",null,[1, 2].map(function (item) {return React.createElement("span", null, item);})), document.getElementById('#root'));ReactDom.render({type: 'div', key: null, props: {children: [{type: 'span', key: null, props: {children: 1}},{type: 'span', key: null, props: {children: 2}}]}}, document.getElementById('#root'));//最后执行render方法渲染为真实dom树<div><span>1</span><span>2</span></div>
于我们而言,最重要的是这个虚拟dom节点:
{type: 'div', key: null, props: {children: [{type: 'span', key: null, props: {children: 1}},{type: 'span', key: null, props: {children: 2}}]}}
如果我们点击删除按钮删除了第一项,则产生新的虚拟dom节点如下:
{type: 'div', key: null, props: {children: [{type: 'span', key: null, props: {children: 2}}]}}
一、首先就是渲染div虚拟节点。从之前虚拟节点树的同辈节点中遍历查找老的虚拟节点,遍历的过程中会不断对比。在与之前第一个div虚拟节点对比中,由于type和key都与新虚拟节点的相同(key都为null),于是会把这个虚拟节点作为新div虚拟节点对应的老节点。如果存在老节点,假如他是字符串类型的节点,会储存着上次渲染的真实dom节点,假如是函数类型的节点,会储存着之前组件的实例,这样就不会再去实例一个组件。由于这儿直接发现了老节点,所以会直接读取老节点储存的真实dom节点作为新div虚拟节点的dom节点,也就是直接使用之前页面中已经存在的div这个节点,这样就不会重新创建一个div类型的dom节点。
二、接下来开始比较虚拟节点div的子节点。从第一个虚拟节点span开始,先查找之前对应的老虚拟节点。由于之前第一个span虚拟节点的type与key和新的span虚拟节点相同,所以进而作为老节点,直接复用之前储存的dom节点,然后比较第一个虚拟节点span的子节点文本2。由于与老节点的子节点中文本节点1不同(文本节点只比较内容),所以会新创建内容为2的文本节点,然后移除之前第一个span的文本节点1。新的虚拟div子节点对比完成后,开始处理没有使用的老虚拟节点,也就是老的第二个span虚拟节点没有被使用,所以会被清空并移除对应的真实dom节点。
综上可知:由于上面没有加key,虽然最后的渲染结果是正常的,但实际渲染流程并不是我们以为的那样。我们只删除了第一个span虚拟节点,所以只要移除对应的第一个真实span节点就行了。而我们分析后发现先创建了文本节点2,然后移除了文本节点1,再移除了第二个真实span节点。我们知道对于真实的dom节点操作是非常昂贵的,比较耗性能。并且如果是类组件,那就意味着会销毁以前已经实例的组件,然后会新实例一次类组件,再执行一遍这个组件的一些初始生命周期。这样diff效率明显不高。
设置key的情况:
//数据为[1,2]的虚拟节点树{type: 'div', key: null, props: {children: [{type: 'span', key: 1, props: {children: 1}},{type: 'span', key: 2, props: {children: 2}}]}}//数据为[2]的虚拟节点树{type: 'div', key: null, props: {children: [{type: 'span', key: 2, props: {children: 2}}]}}
此时当[1,2]变为[2]后,div的虚拟节点渲染还是同样的逻辑。而在比较第一个span虚拟节点时,首先判断先前第一个span虚拟节点{type:’span’,key:1}。虽然type相同但key不同,于是继续从同辈节点中查找,开始与第二个span虚拟节点{type:’span’:key:2}对比。发现type与key都相同,于是把这个节点作为新节点对应的老节点。继续比较内容为2的虚拟节点,发现与老节点中子节点2相同,继而复用,这样就不会创建新的文本节点。接下来发现老的虚拟节点{type:’span’,key:1}已不在使用,所以移除真实的1的节点。
综上可知:设置key后,再次渲染组件时逻辑符合我们的预期。所以设置了key能够发挥diff算法的强大能力,进而提升渲染效率。
常见问题:
1、数组中index可以作为key使用吗?
如果不会涉及到列表中节点位置变动,比如只是更新了一些项的数据,那么用index作为key与不设置key是一样的,都不会对diff影响什么。只不过不设置key会在react开发模式中控制台有警告信息。
其它的情况就不能用index做为key,比如添加或者删除一些项。这会影响节点的位置移动,这时就需要key与项有关联来标识之前的项在何位置,以便增加渲染性能,减少一些意外问题。例如下面的虚拟节点树,当头部新增节点后,如果用index作为key,react在diff过程中会以为更新了前两个节点,并在尾部新增了一个节点。
[{type:'div',key:0,props:{children:'item 0'}},{type:'div',key:1,props:{children:'item 1'}}]//头部新增项后[{type:'div',key:0,props:{children:'item new'}},{type:'div',key:1,props:{children:'item 0'}},{type:'div',key:2,props:{children:'item 1'}}]
2、只有map返回的节点需要设置key吗?
不是,只要是同辈相同类型的节点,都建议设置key属性来提高渲染性能。
例如下面的组件:
<div>{this.state.step === 'base' && <div key="base">{this.getBaseForm()}</div>}{this.state.step === 'fee' && <div key="fee">{this.getFeeForm()}</div>}</div>
**
