虚拟 DOM
当你用传统的源生api或jQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。比如当你在一次操作时,需要更新10个DOM节点,理想状态是一次性构建完DOM树,再执行后续操作。但浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。显然例如计算DOM节点的坐标值等都是白白浪费性能,可能这次计算完,紧接着的下一个DOM更新请求,这个节点的坐标值就变了,前面的一次计算是无用功。
即使计算机硬件一直在更新迭代,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验。真实的DOM节点,哪怕一个最简单的div也包含着很多属性,可以打印出来直观感受一下:
虚拟DOM就是为了解决这个浏览器性能问题而被设计出来的。例如前面的例子,假如一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,通知浏览器去执行绘制工作,这样可以避免大量的无谓的计算量。
虚拟DOM(vdom) 就是用JS来模拟DOM结构的树形结构。
JS模拟DOM结构:
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
</ul>
{
tag: 'ul'
attrs: {
id: 'list'
}
children: [
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 1']
}, {
tag: 'li',
attrs: {className: 'item'},
children: ['Item 2']
}
]
}
模拟React中的render函数:
let render = (eleObj,container)=>{
// 先取出第一层 进行创建真实dom
let {type,props} = eleObj;
let elementNode = document.createElement(type); // 创建第一个元素
for(let attr in props){ // 循环所有属性
if(attr === 'children'){ // 如果是children表示有嵌套关系
if(typeof props[attr] == 'object'){ // 看是否是只有一个文本节点
props[attr].forEach(item=>{ // 多个的话循环判断 如果是对象再次调用render方法
if(typeof item === 'object'){
render(item,elementNode)
}else{ //是文本节点 直接创建即可
elementNode.appendChild(document.createTextNode(item));
}
})
}else{ // 只有一个文本节点直接创建即可
elementNode.appendChild(document.createTextNode(props[attr]));
}
}else if(attr === 'className'){ // 是不是class属性 class 属性特殊处理
elementNode.setAttribute('class',props[attr]);
}else{
elementNode.setAttribute(attr,props[attr]);
}
}
container.appendChild(elementNode)
};
跨平台
虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点
首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
diff 算法
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计;
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构;
- 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
tree diff
基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。
既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildrenElements, transaction, context);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
if (errorThrown) {
clearQueue();
} else {
processQueue();
}
}
}
}
如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?
如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A。
由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作。
component diff
- React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效;
- 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree;
- 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。
element diff
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
- INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作;
- MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 部分DOM 节点;
- REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。
针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!
新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。
diff 加 key 的缺陷
加key唯一标记每个节点,diff算法更好对比(不建议使用:key=index,建议使用:key=id)DOMDiff算法是通过key来判断DOM节点是否变化的。
演示:
没有使用key的时候,你想把第二个方块删除掉,你想要到的结果是剩下三角和圆圈,但是真实的结果却是剩下三角和方框
这是因为,diff算法做了两件事
- 把2变成3;
- 把3删除掉。
这有两个数组[1,2,3] 和 [1,3]。人类会说,这不就是少了个2嘛 ,但是计算机不会直接删除第二个,而是会遍历数组。
- 首先对比1和1,发现[1没变」;
- 然后对比2和3,发现「2变成了3」;
- 最后对比undefined和3,发现[3被删除了」。 所以计算机的结论是:「2变成了 3」以及「3被删除了」
解决方法:
就是给每一个元素一个id,用id做key值:
如果你用index作为key,那么在删除第二项的时候,index 就会从1、2、3变成1、2(因为index永远都是连续的,所以不可能是1、3)。所以,永远不要用index作为key.