写作不易,未经作者允许禁止以任何形式转载!
如果觉得文章不错,欢迎关注、点赞和分享!原文链接:
Vue进阶 Diff算法详解
一、虚拟DOM
什么是虚拟DOM?
虚拟DOM就是把真实DOM树的结构和信息抽象出来,以对象的形式模拟树形结构,如下:
真实DOM:
<div>
<p>Hello World</p>
</div>
对应的虚拟DOM就是:
let vnode = {
tag: 'div',
children:[ {
tag:'p', text:'Hello World'}]
}
为什么需要虚拟DOM?
渲染真实DOM会有一定的开销,如果每次修改数据都进行真实DOM渲染,都会引起DOM树的重绘和重排,性能开销很大。那么有没有可能只修改一小部分数据而不渲染整个DOM呢?虚拟DOM和Diff算法可以实现。
怎么实现?
- 先根据真实DOM生成一颗虚拟DOM树
- 当某个DOM节点数据发生改变时,生成一个新的Vnode
- 新的Vnode和旧的oldVnode进行对比
- 通过patch函数一边比对一边给真实DOM打补丁或者创建Vnode、移除oldVnode等
有什么不一样?
- 真实DOM操作为一个属性一个属性去修改,开销较大。
- 虚拟DOM直接修改整个DOM节点再替换真实DOM
还有什么好处?
Vue的虚拟DOM数据更新机制是异步更新队列,并不是数据变更马上更新DOM,而是被推进一个数据更新异步队列统一更新。想要马上拿到DOM更新后DOM信息?有个API叫 Vue.nextTick
二、 Diff算法
传统Diff算法
遍历两棵树中的每一个节点,每两个节点之间都要做一次比较。
比如 a->e 、a->d 、a->b、a->c、a->a
- 遍历完成的时间复杂度达到了O(n^2)
- 对比完差异后还要计算最小转换方式,实现后复杂度来到了O(n^3)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W9Ydnr4l-1617040179713)(https://bloginfo.lebronchao.com/doc-image/(null)]-20210326001324690.png)
Vue优化的Diff算法
Vue的diff算法只会比较同层级的元素,不进行跨层级比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxgOiniZ-1617040179718)(https://bloginfo.lebronchao.com/doc-image/(null)]-20210324234529318-20210326001327579.(null))
三、 Vue中的Diff算法实现
Vnode分类
- EmptyVNode: 没有内容的注释节点
- TextVNode: 文本节点
- ElementVNode: 普通元素节点
- ComponentVNode: 组件节点
- CloneVNode: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned属性为true
Patch函数
patch函数接收以下参数:
- oldVnode:旧的虚拟节点
- Vnode:新的虚拟节点
- hydrating:是否要和真实DOM混合
- removeOnly:特殊的flag,用于 transition-group
处理流程大致分为以下步骤:
- vnode不存在,oldVnode存在时,移除oldVnode
- vnode存在,oldVnode不存在时,创建vnode
- vnode和oldVnode都存在时
- 如果vnode和oldVnode是同一个节点(通过sameVnode函数对比 后续详解),通过patchVnode进行后续比对工作
- 如果vnode和oldVnode不是同一个节点,那么根据vnode创建新的元素并挂载至oldVnode父元素下。如果组件根节点被替换,遍历更新父节点element。然后移除旧节点。如果oldVnode是服务端渲染元素节点,需要用hydrate函数将虚拟dom和真是dom进行映射
源码如下,已写好注释便于阅读
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// 如果vnode不存在,但是oldVnode存在,移除oldVnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 如果oldVnode不存在,但是vnode存在时,创建vnode
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 剩余情况为vnode和oldVnode都存在
// 判断是否为真实DOM元素
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 如果vnode和oldVnode是同一个(通过sameVnode函数进行比对 后续详解)
// 受用patchVnode函数进行后续比对工作 (函数后续详解)
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
// vnode和oldVnode不是同一个的情况
if (isRealElement) {
// 如果存在真实的节点,存在data-server-render属性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
// 当旧的Vnode是服务端渲染元素,hydrating记为true
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// 需要用hydrate函数将虚拟DOM和真实DOM进行映射
if (isTrue(hydrating)) {
// 需要合并到真实DOM上
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
// 调用insert钩子
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <