1.虚构dom
原生的JS DOM操作十分耗费性能,而React把实在原生JS DOM转换成了JavaScript对象。这就是虚构Dom(Virtual Dom)
每次数据更新后,从新计算虚构Dom,并和上一次生成的虚构dom进行比照,对发生变化的局部作批量更新。在此其中,React提供了componentShouldUpdate生命周期来让开发者手动管制缩小数据变动后不必要的虚构dom比照,晋升性能和渲染效率。
原生html元素代码:
<div class="title"> <span>Hello ConardLi</span> <ul> <li>苹果</li> <li>橘子</li> </ul></div>
在React可能存储为这样的JS代码:
const VitrualDom = { type: 'div', props: { class: 'title' }, children: [ { type: 'span', children: 'Hello ConardLi' }, { type: 'ul', children: [ { type: 'li', children: '苹果' }, { type: 'li', children: '橘子' } ] } ]}
当咱们须要创立或更新元素时,React首先会让这个VitrualDom对象进行创立和更改,而后再将VitrualDom对象渲染成实在DOM;
当咱们须要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。
虚构DOM的组成:
通过JSX或React.createElement,React.createClass等形式创立虚构元素和组件。即ReactElementelement对象,咱们的组件最终会被渲染成上面的构造:
- type:元素的类型,能够是原生html类型(字符串),或者自定义组件(函数或class)
- key:组件的惟一标识,用于Diff算法,上面会具体介绍
- ref:用于拜访原生dom节点
- props:传入组件的props,chidren是props中的一个属性,它存储了以后组件的孩子节点,能够是数组(多个孩子节点)或对象(只有一个孩子节点)
- owner:以后正在构建的Component所属的Component
- self:(非生产环境)指定以后位于哪个组件实例
- _source:(非生产环境)指定调试代码来自的文件(fileName)和代码行数(lineNumber)
<div className="title"> <span>Hello ConardLi</span> <ul> <li>苹果</li> <li>橘子</li> </ul></div>
将此JSX元素打印进去,证实虚构DOM实质就是js对象:
其中,在jsx中应用的原生元素标签,其type为标签名。而如果是函数组件或class组件,其type就是对应的class或function对象参考 前端react面试题具体解答
2.diff算法
React须要同时保护两棵虚构DOM树:一棵示意以后的DOM构造,另一棵在React状态变更将要从新渲染时生成。React通过比拟这两棵树的差别,决定是否须要批改DOM构造,以及如何批改。这种算法称作Diff算法。
这个算法问题有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作数。 然而,即便在最前沿的算法中,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。
如果在 React 中应用了该算法,那么展现 1000 个元素所须要执行的计算量将在十亿的量级范畴。这个开销切实是太过昂扬。于是 React 在以下两个假如的根底之上提出了一套 O(n) 的启发式算法:
1:两个不同类型的元素会产生出不同的树;
2:开发者能够通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;
React diff算法大抵执行过程:
Diff算法会对新旧两棵树做深度优先遍历,防止对两棵树做齐全比拟,因而算法复杂度能够达到O(n)。而后给每个节点生成一个惟一的标记:
在遍历的过程中,每遍历到一个节点,就将新旧两棵树作比拟,并且只对同一级别的元素进行比拟:
也就是只比拟图中用虚线连接起来的局部,把前后差别记录下来。
React diff算法具体策略:
(1)tree diff
tree diff次要针对的是React dom节点跨层级的操作。因为跨层级的DOM挪动操作较少,所以React diff算法的tree diff没有针对此种操作进行深刻比拟,只是简略进行了删除和创立操作
如图所示,A 节点(包含其子节点)整个被挪动到 D 节点下,因为 React 只会简略地思考同层级节点的地位变换,而对于不同层级的节点,只有创立和删除操作。
当根节点发现子节点中 A 隐没了,就会间接销毁 A;当 D 发现多了一个子节点 A,则会创立新的 A(包含子节点)作为其子节点。此时,diff 的执行状况:create A → create B → create C → delete A
由此能够发现,当呈现节点跨层级挪动时,并不会呈现设想中的挪动操作,而是以 A 为根节点的整个树被从新创立。这是一种影响 React 性能的操作,因而官网倡议不要进行 DOM 节点跨层级的操作。
基于上述起因,在开发组件时,保持稳定的 DOM 构造会有助于性能的晋升。例如,能够通过 CSS 暗藏或显示节点,而不是真正地移除或增加 DOM 节点
(2)component diff:
component diff是专门针对更新前后的同一层级间的React组件比拟的diff 算法:
- 如果是同一类型的组件,依照原策略持续比拟 Virtual DOM 树(例如持续比拟组件props和组件里的子节点及其属性)即可。
- 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点,即销毁原组件,创立新组件。
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变动,如果可能确切晓得这点,那么就能够节俭大量的 diff 运算工夫。因而,React 容许用户通过 shouldComponentUpdate()来判断该组件是否须要进行 diff 算法剖析
如图 所示,当组件 D 变为组件 G 时,即便这两个组件构造类似,一旦 React 判断 D 和G 是不同类型的组件,就不会比拟二者的构造,而是间接删除组件 D,从新创立组件 G 及其子节点。
尽管当两个组件是不同类型但构造类似时,diff 会影响性能,但正如 React 官网博客所言:不同类型的组件很少存在类似 DOM树的状况,因而这种极其因素很难在理论开发过程中造成重大的影响
(3)element diff
element diff是专门针对同一层级的所有节点(包含元素节点和组件节点)的diff算法。当节点处于同一层级时,diff 提供了 3 种节点操作,别离为 INSERT_MARKUP(插入)
、MOVE_EXISTING(挪动)
和 REMOVE_NODE(删除)
。
咱们将虚构dom树中欲比拟的某同一层级的所有节点的汇合别离称为新汇合和旧汇合,则有以下策略:
- INSERT_MARKUP:新汇合的某个类型组件或元素节点不存在旧汇合里,即全新的节点,须要对新节点执行插入操作。
- MOVE_EXISTING:新汇合的某个类型组件或元素节点存在旧汇合里,且 element 是可更新的类型,generateComponent-Children 已调用receiveComponent,这种状况下 prevChild=nextChild,就须要做挪动操作,能够复用以前的 DOM 节点。
- REMOVE_NODE:旧汇合的某个组件或节点类型,在新汇合里也有,但对应的 element 不同则不能间接复用和更新,须要执行删除操作,或者旧组件或节点不在新汇合里的,也须要执行删除操作。
如图 所示,旧汇合中蕴含节点A、B、C 和 D,更新后的新汇合中蕴含节点 B、A、D 和C(只是产生了地位变动,各自节点以及外部数据没有变动),此时新旧汇合按程序进行逐个的diff 差异化比照,发现 B != A,则创立并插入 B 至新汇合,删除旧汇合 A;以此类推,创立并插入 A、D 和 C,删除 B、C 和 D。
React 发现这类操作繁缛冗余,因为这些都是雷同的节点,但因为地位程序发生变化,导致须要进行繁冗低效的删除、创立操作,其实只有对这些节点进行地位挪动即可。
针对这一景象,React 提出优化策略:容许开发者对同一层级的同组子节点,增加惟一 key 进行辨别,。见上面key机制
3. key机制
(1)key的作用
当同一层级的某个节点增加了对于其余同级节点惟一的key属性,当它在以后层级的地位产生了变动后。react diff算法通过新旧节点比拟后,如果发现了key值雷同的新旧节点,就会执行挪动操作(而后仍然按原策略深刻节点外部的差别比照更新),而不会执行原策略的删除旧节点,创立新节点的操作。这无疑大大提高了React性能和渲染效率
(2)key的具体执行过程
首先,对新汇合中的节点进行循环遍历 for (name in nextChildren),通过惟一的 key 判断新旧汇合中是否存在雷同的节点 if (prevChild === nextChild),如果存在雷同节点,则进行挪动操作,但在挪动前须要将以后节点在旧汇合中的地位与 lastIndex 进行比拟 if (child._mountIndex < lastIndex),否则不执行该操作。
例子1:同一层级的所有节点只产生了地位变动:
按新汇合中程序开始遍历
- B在新汇合中 lastIndex(相似浮标) = 0, 在旧汇合中 index = 1,index > lastIndex 就认为 B 对于汇合中其余元素地位无影响,不进行挪动,之后lastIndex = max(index, lastIndex) = 1
- A在旧汇合中 index = 0, 此时 lastIndex = 1, 满足 index < lastIndex, 则对A进行挪动操作,此时lastIndex = max(Index, lastIndex) = 1
- D和B操作雷同,同(1),不进行挪动,此时lastIndex=max(index, lastIndex) = 3
- C和A操作雷同,同(2),进行挪动,此时lastIndex = max(index, lastIndex) = 3
上述论断中的挪动操作即对节点进行更新渲染,而不进行挪动则示意无需更新渲染
例子2:同一层级的所有节点产生了节点增删和节点地位变动:
- 同下面那种情景,B不进行挪动,lastIndex=1
- 新汇合中获得E,发现旧中不存在E,在 lastIndex处创立E,lastIndex++
- 在旧汇合中取到C,C不挪动,lastIndex=2
- 在旧汇合中取到A,A挪动到新汇合中的地位,lastIndex=2
- 实现新汇合中所有节点diff后,对旧汇合进行循环遍历,寻找新汇合中不存在但就汇合中的节点(此例中为D),删除D节点。
(3)index作为key
react中经常会用到通过遍历(如Array.map)来在以后层级动静生成多个子节点的操作。这是常见的列表数据渲染场景。
React官网倡议不要用遍历的index作为这种场景下的节点的key属性值。比方以后遍历的所有节点类型都雷同,其外部文本不同,在用index作key的状况下,当咱们对原始的数据list进行了某些元素的程序扭转操作,导致了新旧汇合中在进行diff比拟时,雷同index所对应的新旧的节点其文本不统一了,就会呈现一些节点须要更新渲染文本,而如果用了其余稳固的惟一标识符作为key,则只会产生地位程序变动,无需更新渲染文本,晋升了性能。
此外应用index作为key很可能会存在一些出乎意料的显示谬误的问题:
{this.state.data.map((v,index) => <Item key={index} v={v} />)}// 开始时:['a','b','c']=><ul> <li key="0">a <input type="text"/></li> <li key="1">b <input type="text"/></li> <li key="2">c <input type="text"/></li></ul>// 数组重排 -> ['c','b','a'] =><ul> <li key="0">c <input type="text"/></li> <li key="1">b <input type="text"/></li> <li key="2">a <input type="text"/></li></ul>
下面实例中在数组从新排序后,key对应的实例都没有销毁,而是从新更新。具体更新过程咱们拿key=0的元素来阐明, 数组从新排序后:
- 组件从新render失去新的虚构dom;
- 新老两个虚构dom进行diff,新老版的都有key=0的组件,react认为同一个组件,则只可能更新组件;
- 而后比拟其children,发现内容的文本内容不同(由a--->c),而input组件并没有变动,这时触发组件的componentWillReceiveProps办法,从而更新其子组件文本内容;
- 因为组件的children中input组件没有变动,其又与父组件传入的任props没有关联,所以input组件不会更新(即其componentWillReceiveProps办法不会被执行),导致用户输出的值不会变动。
(4)key机制的毛病
如图 所示,若新汇合的节点更新为 D、A、
B、C,与旧汇合相比只有 D 节点挪动,而 A、B、C 依然放弃原有的程序,实践上 diff 应该只需对 D 执行挪动操作,然而因为 D 在旧汇合中的地位是最大的,导致其余节点的 _mountIndex <lastIndex,造成 D 没有执行挪动操作,而是 A、B、C 全副挪动到 D 节点前面的景象.
在开发过程中,尽量减少相似将最初一个节点挪动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在肯定水平上会影响 React 的渲染性能。。
(5)key应用注意事项:
- 如果遍历的列表子节是作为纯展现,而不波及到列表元素程序的动静变更,那应用index作为key还是没有问题的。
- key只是针对同一层级的节点进行了diff比拟优化,而跨层级的节点相互之间的key值没有影响
- 大部分状况下,通过遍历的同一层级的应用了key属性的元素节点其节点类型是雷同的(比方都是span元素或者同一个组件)。如果存在新旧汇合中,雷同的key值所对应的节点类型不同(比方从span变成div),这相当于齐全替换了旧节点,删除了旧节点,创立了新节点。
- 如果新汇合中,呈现了旧汇合没有存在过的key值。例如某个节点的key之前为1,当初为100,但旧汇合中其余节点也没有应用100这个key值。阐明没产生过挪动操作,此时diff算法会对对应的节点进行销毁并从新创立。这在一些场景中会比拟有用(比方重置某个组件的状态)
- key值在比拟之前都会被执行toString()操作,所以尽量不要应用object类型的值作为key,会导致同一层级呈现key值雷同的节点。key值反复的同一类型的节点或组件很可能呈现拷贝反复外部子元素的问题