虚拟 DOM

虚拟DOM 长什么样子

一个标签为div,它的子元素为两个span,className为red,点击事件调用一个函数的DOM。

React

  1. const vNode = {
  2. key:null,
  3. props:{
  4. children:[ // 子元素们
  5. { type:'span',...},
  6. {type:'span',...}
  7. ],
  8. className : "red" // 标签上的属性
  9. onClick:()=>{} // 事件
  10. },
  11. ref:null,
  12. type:"div", //标签名or组件名
  13. ...
  14. }

Vue

  1. const vNode = {
  2. tag: "div", // 标签名 or 组件名
  3. data: {
  4. class: "red", // 标签上的属性
  5. on: {
  6. click: () => { } // 事件
  7. }
  8. },
  9. children: [ // 子元素们
  10. { tag: "span", ... },
  11. { tag: "span", ... }
  12. ],
  13. ...
  14. }

如何创建虚拟DOM

React.createElement

  1. createElement('div', { className: 'red', onClick: () => { } }, [
  2. createElement('span', {}, 'span1'),
  3. createElement('span', {}, 'span2')
  4. ]
  5. )

Vue(只能在 render 函数里得到 h)

  1. h('div', {
  2. class: 'red',
  3. on: {
  4. click: () => { }
  5. },
  6. }, [h('span', {}, 'span1'), h('span', {}, 'span2'])

用 JSX 简化创建虚拟

这种方法严重依赖打包工具

React

通过 babel 转为 createElement 形式

  1. <div className="red" onClick={fn}>
  2. <span>span1</span>
  3. <span>span2</span>
  4. </div>

Vue Template

通过 vue-loader 转为 h 形式

  1. <div class="red" @click="fn">
  2. <span>span1</span>
  3. <span>span2</span>
  4. </div>

可能很多人都听过一句话【DOM操作慢】,却不知其原因,我们现在就来探讨这个问题。

其实单单说DOM操作慢是不够严谨的,严格意义来说应该是在某些情况下虚拟DOM比真实DOM快

某些情况】指下面两种:

  1. 假设你一个接一个的添加了1000个节点,DOM对布局进行了1000次重新计算和重新渲染;而虚拟DOM在单独的DOM树中进行更改,将多次操作合并为一次,只在最后进行一次更大布局计算和重新渲染,总体上减少了计算量。
  2. 假设有1000个节点,其中只有十个是新增的,但DOM依旧会重新渲染整个列表;而虚拟DOM借助DOM diff可以把多余的操作省掉,只把新增的十个节点放进页面里。

虚拟DOM不仅能减少不必要的性能消耗,它还可以跨平台,不仅可以变成DOM,还可以变成小程序、ios应用、安卓应用,因为虚拟DOM本质上只是一个JS对象,那么我们可以把这个JS对象变成元素等。
**

总结

优点

  • 能减少不必要的DOM操作
  • 能跨平台渲染

    缺点

  • 需要额外的创建函数,如createElement或h

  • 可以通过JSX来简化成XML写法,但这种写法很依赖打包工具

DOM diff

虚拟DOM的对比算法

把虚拟 DOM 想象成树形

我们把DOM想象成树形,来看一下DOM diff的大概逻辑。

  1. <div :class="x">
  2. <span v-if="y">{string1}</span>
  3. <span>{string2}</span>
  4. </div>

image.png

DOM diff的大概逻辑Tree diff

  • 将新旧两棵树逐层对比,找出哪些节点需要更新
  • 如果节点是组件就看 Component diff
  • 如果节点是标签就看 Element diff

    Component diff

  • 如果节点是组件,就先看组件类型

  • 类型不同直接替换(删除旧的)
  • 类型相同则只更新属性
  • 然后深入组件做Tree diff(递归)

    Element diff

  • 如果节点是原生标签,则看标签名

  • 标签名不同直接替换,相同只更新属性
  • 然后进入标签后代做Tree diff(递归)

举例

我们举两个例子来看看

x 从 red 变成 green

image.png

  • DOM diff 发现
    • div 标签类型没变,只需要更新 div 对应的 DOM 的属性
    • 子元素没变,不更新

y 从 true 变成 false

image.png

  • DOM diff 发现
    • div 没变,不用更新
    • 子元素1标签没变,但是children变了,更新 DOM 内容
    • 子元素2不见了,删除对应的 DOM

DOM diff 的问题

刚刚的第二个例子有一个问题,明明按照我们的想法,应该直接把子元素1直接删除,让子元素2成为子元素1,但实际上却没有这样,这是为什么呢?
原因是计算机会通过遍历对比。

我们对比两个数组理解一下:

  • 有两个数组:[1,2,3]和[1,3]
  • 首先对比1和1,发现「1没变」,然后对比2和3,发现「2变成了3」,最后对比 undefined和3,发现「3被删除了」。

DOM diff中key的作用

假设我们有三个组件,第一个是皮卡丘,第二个是妙蛙种子,第三个是小火龙。
image.png
点击delete把妙蛙种子删除,但是妙蛙种子并没有被删除,而是2消失了,妙蛙种子变成了3。
image.png
原因很简单,你认为你删除了2,但它会认为你做了两件事:

  • 把2变成了3
  • 然后把3删除了

这就和我们刚刚举得例子是同一个道理。这时候就轮到key上场了。

我们用id作为key再来试一遍

image.png

  • 原本的数组是[id: 1, value: 1 }, { id: 2, value: 2 }, { id: 3, value: 3 }]
  • 点击删除之后的数组是[{ id: 1, value: 1 }, { id: 3, value: 3 }]
  • 然后对比一下:
  • 首先发现id从1 2 3变成了1 3,说明第二项被删除了
  • 然后依次对此 id: 1的项和id: 3的项,发现没变化。