尝试解释Vue的虚拟DOM

2019-08-12

虚拟dom本质上是一种优化方式。天天看见人讨论虚拟dom,其热度在vue模块中好像是最高的,似乎没有虚拟dom各个前端框架就用不了了一样,实际上不是这样。

上一次讨论了侦测变化,实现了在变化发生时程序进行响应。那从原理上说,每次发生变化时,直接重新渲染真实的dom,其实也能达到目标效果。只是渲染真实dom这个操作是浏览器上最耗性能的操作,虚拟dom是提出一种优化方式:建立一层中间层,当数据发生改变时,比较这个中间层的变化,去得到一个和数据相关的dom最小变化量,再去渲染这个变化量,从而提高性能。

原理

目标:将虚拟节点(vnode)渲染到视图上

优势:通过vnode和真实节点比较,只渲染变化的部分,性能提高

VNode

描述真实DOM节点的JS对象,每次渲染视图时使用vnode创建真实DOM插入到视图中,除了和真实DOM节点对应的vnode之外,还有特殊的:

  • 克隆节点,从一个vnode上全属性克隆,用于优化如静态节点的情况
  • 组件节点,描述Vue组件的vnode,包括组件的options,组件对应的vue实例等
  • 函数式节点,描述函数式组件

Patch

用于对比节点树差异的算法,针对不同的情况进行更新,提升性能:

  • 新增节点创建
  • 废弃节点删除
  • 更新节点修改

输入:vnode、oldVnode(上一次渲染DOM时所创建的vnode)

输出:渲染操作

渲染操作

  • 创建节点:判断vnode是否为需要创建真实DOM的类型(元素节点、注释节点、文本节点),调用createElement和appendChild等dom方法进行渲染
  • 删除节点:找到需要删除的节点的父节点,使用removeChild即可
  • 更新节点:当vnode和oldVnode不同时,会调用更新算法
    • 如果是静态节点(不监听任何数据状态的节点),直接跳过
    • 如果是文本节点(有text属性),直接更新,不去比较差别
    • vnode有children属性(有子节点)
      • oldVnode无children属性,直接递归创建所有的children节点
      • oldVnode有children属性,需要递归比较vnode和oldVnode的子节点,根据具体情况产生更加具体的操作(节点移动、删除、新增、更新),这部分在下一小节具体说明。

节点更新操作

当vnode的子节点和oldVnode的子节点都存在且不同时,就会发生节点更新操作,包括节点移动、删除、新增、更新。

对于更新操作,最基本的就是去递归遍历(可先看作从左到右循环处理)vnode的子节点,每一个子节点都与oldVnode的对应位置进行比较,根据具体情况执行不同的操作。

  • 创建子节点:vnode中有某个子节点,而oldVnode中没有时,创建一个对应的DOM到所有“未处理”,即还没有检查循环到的DOM节点之前。
  • 更新节点:vnode中有某个子节点,oldVnode中的对应位置也有相同子节点。前文已说明这种情况,递归更新操作。
  • 移动节点:vnode中有某个子节点,oldVnode中也有相同子节点但不在对应位置,则将其移动到所有未处理节点的最前面
  • 删除节点:当循环递归遍历完成后,oldVnode中还存在的节点,则直接删除掉对应的DOM

对每个节点都循环遍历寻找对应节点是比较耗性能的,通常查找对应节点这个操作可以优化,从经验上来看,首先查找vnode和oldVnode中未处理的最前最后的对应四个节点(新前、新后、旧前、旧后)是否对应,如果没找到再进行循环查找。

事实上,使用这种优化方式后,循环就并非单向了,而是双向的。一旦vnode或oldVnode有一个循环结束,则该vnode更新完成。例如oldVnode循环完成,vnode中未循环完的直接全部当新节点插入。