虚拟dom的作用到底是什么,真的是为了加快效率嘛

670 阅读4分钟

本编,博主将从虚拟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对象的速度。

image.png 但是就算操作js对象的速度要快,最终不是还需要操作真实dom进行更新吗,这不是多此一举吗,需要先操作一遍虚拟dom,在操作真实dom,这不是多出了一个步骤吗。 这是因为在vuereact这种框架中无法直接定位变化的那个元素,它的细粒度是组件级别的,所以说需要对比更新前后的虚拟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.children
    const newChildren = n2.children

    for (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差异过大,更新的开销更大。

image.png 很明显,我们肯定是想要右边那种复用方式,那么如何实现呢,就需要用到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.children
  const newChildren = n2.children

  for (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,在不同的平台生成相同的界面。

小知识 vue3.6好想要抛弃虚拟dom,Svelte,SolidJS是无虚拟dom框架, 可以直接定位要更新的元素节点,速度相对vue,react更快,但是相对跨平台性更差。