vue源码解读--Vue中的虚拟DOM

130 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

参考自Vue源码系列-Vue中文社区

虚拟DOM简介

虚拟DOM是把真实DOM节点用原生 js 代码抽象成一个个对象。

<div class="a" id="b">我是内容</div>
​
{
  tag:'div',        // 元素标签
  attrs:{           // 属性
    class:'a',
    id:'b'
  },
  text:'我是内容',  // 文本内容
  children:[]       // 子元素
}

此时这个 js 对象就可以用来描述这个 div 节点,把它称作这个真实DOM节点的虚拟DOM节点。

为什么要有虚拟DOM

数据刷新就不能避免操作DOM,而操作真实DOM节点是非常消耗性能的,因为一个DOM元素有特别多的属性。

const div = document.createElement("div")
let str = "";
for (key in div) {
    str += key;
}
console.log(str);

image-20220814160829708.png

打印一个空 div 标签就有这么多属性,如果有复杂嵌套的 DOM节点操作起来会更消耗性能。但是视图更新肯定就会操作 DOM ,所以只能尽可能减少 DOM 操作。

JS 对象模拟出 DOM 节点,称为虚拟 DOM 节点。当数据发生变化时,对比变化前后的虚拟 DOM 节点,通过 DOM-Diff 算法计算出需要更新的地方,然后去更新需要更新的视图,这是用 JS 的计算性能来换取操作 DOM 的性能。

Vue中的虚拟DOM

VNode类

在Vue中有一个 VNode 类,通过这个类把不同的节点类型实例化成虚拟 DOM 节点:

// 源码位置:src/core/vdom/vnode.js
​
export default class VNode {
  constructor(
    tag,
    data,
    children,
    text,
    elm,
    context,
    componentOptions,
    asyncFactory
  ) {
    this.tag = tag; /*当前节点的标签名*/
    this.data =
      data; /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.children = children; /*当前节点的子节点,是一个数组*/
    this.text = text; /*当前节点的文本*/
    this.elm = elm; /*当前虚拟节点对应的真实dom节点*/
    this.ns = undefined; /*当前节点的名字空间*/
    this.context = context; /*当前组件节点对应的Vue实例*/
    this.fnContext = undefined; /*函数式组件对应的Vue实例*/
    this.fnOptions = undefined;
    this.fnScopeId = undefined;
    this.key = data && data.key; /*节点的key属性,被当作节点的标志,用以优化*/
    this.componentOptions = componentOptions; /*组件的option选项*/
    this.componentInstance = undefined; /*当前节点对应的组件的实例*/
    this.parent = undefined; /*当前节点的父节点*/
    this.raw = false; /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.isStatic = false; /*静态节点标志*/
    this.isRootInsert = true; /*是否作为跟节点插入*/
    this.isComment = false; /*是否为注释节点*/
    this.isCloned = false; /*是否为克隆节点*/
    this.isOnce = false; /*是否有v-once指令*/
    this.asyncFactory = asyncFactory;
    this.asyncMeta = undefined;
    this.isAsyncPlaceholder = false;
  }
​
  get child() {
    return this.componentInstance;
  }
}

VNode 类把一个真实 DOM 节点的必要属性保存起来,通过不同的属性值,就可以描述出各种类型的真实 DOM 节点。

VNode的类型

VNode 描述了下面几种节点类型:

注释节点
// 创建注释节点
export const createEmptyVNode = (text = "") => {
  const node = new VNode();
  node.text = text;
  node.isComment = true;
  return node;
};

描述一个注释节点只需两个属性,text表示注释的内容,isComment是一个标志,有这个属性说明该节点是注释节点。

文本节点
// 创建文本节点
export function createTextVNode(val) {
  return new VNode(undefined, undefined, undefined, String(val));
}

文本节点只需要一个 text 属性,即文本内容。

克隆节点
// 创建克隆节点
export function cloneVNode(vnode){
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  );
  cloned.ns = vnode.ns;
  cloned.isStatic = vnode.isStatic;
  cloned.key = vnode.key;
  cloned.isComment = vnode.isComment;
  cloned.fnContext = vnode.fnContext;
  cloned.fnOptions = vnode.fnOptions;
  cloned.fnScopeId = vnode.fnScopeId;
  cloned.asyncMeta = vnode.asyncMeta;
  cloned.isCloned = true;
  return cloned;
}

克隆节点是把已存在的节点的属性全部复制到新节点中,然后会新增一个属性 isCloned ,表示该节点克隆节点。

元素节点
// 真实DOM节点
<div id='a'><span>难凉热血</span></div>
​
// VNode节点
{
  tag:'div',
  data:{},
  children:[
    {
      tag:'span',
      text:'难凉热血'
    }
  ]
}

元素节点和真实 DOM 节点很相似,有 tag 属性,描述标签属性的 data 属性,描述子节点信息的 children 属性等。

组件节点

组件节点除了元素节点具有的属性之外,还有两个特有的属性:

  1. componentOptions:组件的option选项,如组件的 props
  2. componentInstance:当前组件对应的 Vue 实例
函数式组件节点

函数式组件节点,比组件节点还多两个特有的属性:

  1. fnContext:函数时组件对应的 Vue 实例
  2. fnOptions:组件的option选项

VNode的作用

在视图渲染之前,会把 template 模板先编译成 VNode 并缓存下来,数据发生变化后需要更新视图时,把数据变化后生成的 VNode 与之前缓存的 VNode 进行对比,然后找出对应的有差异的 DOM 节点就是我们需要更新的 DOM 节点,最后创建出真实 DOM 节点插入到视图中,完成视图更新。

总结

  1. 虚拟 DOM是把真实 DOM抽象成一个 JS 对象。
  2. 操作真实 DOM 非常消耗性能,为了减少性能消耗,需要尽可能的减少 DOM 操作,虚拟 DOM 使用 JS 的计算性能来换取操作 DOM 的性能。
  3. VNode 类,根据不同节点类型,生成不同类型的 VNode 实例。
  4. 通过数据变化前后的 VNode 实例对比找出差异,只更新有差异的视图,尽可能的减少操作真实 DOM