虚拟dom是什么,他有什么好处
本编,博主将从虚拟dom是什么引出,为什么需要虚拟dom, 虚拟dom的益处 , 为什么需要Diff算法,for循环中key的作用是什么。
1.虚拟dom是什么
虚拟dom就是以js对象的形式表示真实dom结构
例如
const newVNode = {type: 'div',children: [{ type: 'p', children: '4' },{ type: 'p', children: '5' },{ type: 'p', children: '6' }]
}
很明显能看出来,这个虚拟dom描述的其实就是,外层容器是一个div,有三个子标签p
<div><p>4</p><p>5</p><p>6</p>
</div>
2.那为什么需要虚拟dom呢
首先就是操作真实dom的速度要远低于操作js对象的速度。
但是就算操作js对象的速度要快,最终不是还需要操作真实dom进行更新吗,这不是多此一举吗,需要先操作一遍虚拟dom,在操作真实dom,这不是多出了一个步骤吗。
这是因为在vue
和react
这种框架中无法直接定位变化的那个元素,它的细粒度是组件级别的,所以说需要对比更新前后的虚拟dom找不同,在更新不同。
例如
// 旧的虚拟 DOM(旧 vnode)
const oldVNode = {type: 'div',children: [{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 },{ type: 'p', children: '3', key: 3 }]
}// 新的虚拟 DOM(新 vnode)
const newVNode = {type: 'div',children: [{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 },{ type: 'p', children: '6', key: 3 }]
}
看这两个新旧虚拟dom,就是要更新的是什么,肯定是更新最后一个p标签。
这里的找不同进行更新就是diff算法
diff算法
我们知道操作真实dom的效率太低,那就应该尽量减少操作真实dom的次数
所以diff出现的目的就是为了复用dom,减少操作真实dom的次数
比如上面新旧dom树,我们就可以复用第一和第二p标签,第三个p标签单独更新即可,所以说前两个可以复用,第三个需要更新就是不能复用吗,并不是这样,我们发现,第三个P标签只需要更新文本节点即可,所以第三个标签也是可复用标签,只需要更新其文本就行。
那么diff落实到代码是如何实现dom的复用呢
还是用上面的那个新旧虚拟dom
// 旧的虚拟 DOM(旧 vnode)
const oldVNode = {type: 'div',children: [{ type: 'p', children: '1'},{ type: 'p', children: '2'},{ type: 'p', children: '3'}]
}// 新的虚拟 DOM(新 vnode)
const newVNode = {type: 'div',children: [{ type: 'p', children: '1'},{ type: 'p', children: '2'},{ type: 'p', children: '6'}]
}
真实的dom算法比较复杂,我们这里就模拟一个简单的diff。
function patchChildren(n1, n2) {const oldChildren = n1.childrenconst newChildren = n2.childrenfor (let i = 0; i < oldChildren.length; i++) {//复用dom进行更新patch(oldChildren[i], newChildren[i])}
}
代码很简单,就是一个一个更新,这里的patch就是更新操作,不用管他具体实现,只需要知道它的作用是复用更新即可。
这样就是无脑更新,我们在把虚拟dom的顺序变一下
// 旧的虚拟 DOM(旧 vnode)
const oldVNode = {type: 'div',children: [{ type: 'p', children: '1'},{ type: 'p', children: '2'},{ type: 'p', children: '3'},]
}// 新的虚拟 DOM(新 vnode)
const newVNode = {type: 'div',children: [{ type: 'p', children: '3'},{ type: 'p', children: '2'},{ type: 'p', children: '1'}]
}
如果我们依然采用上面那种方式更新的话,就会造成复用的dom差异过大,更新的开销更大。
很明显,我们肯定是想要右边那种复用方式,那么如何实现呢,就需要用到key了
// 加了key熟悉的旧的虚拟 DOM(旧 vnode)
const oldVNode = {type: 'div',children: [{ type: 'p', children: '1', key: 1},{ type: 'p', children: '2', key: 2},{ type: 'p', children: '3', key: 3},]
}
然后打乱旧vnode的顺序,成为新vnode
// 新的虚拟 DOM(新 vnode)
const newVNode = {type: 'div',children: [{ type: 'p', children: '3', key: 3},{ type: 'p', children: '2', key: 2},{ type: 'p', children: '1', key: 1}]
}
然后在写一下新版本的diff算法
function patchChildren(n1, n2) {const oldChildren = n1.childrenconst newChildren = n2.childrenfor (let i = 0; i < oldChildren.length; i++) {//复用dom进行更新const fu_use_dom = oldChildren.find(item => item.key == newChildren.key)patch(fu_use_dom, newChildren[i])}
}
这样一来我们就可以实现更好的复用,这个就是diff算法中key的作用,作为虚拟dom节点的标识,利于diff更新过程,找到正确复用的dom。
另一个好处就是跨平台
2.跨平台
我们这个js在很多平台,都支持,但是不同的平台,操作界面的方式可能不一样,浏览器是操作dom,但是别的可能不是dom,但是我们通过虚拟dom就能知道我最终要生成的界面长什么样子,只需要在不同平台,采取对应平台的渲染方式,就可以做到一套虚拟dom,在不同的平台生成相同的界面。