VUE中的虚拟DOM

1.前言

虚拟DOM,这个名词作为当下的前端开发人员你一定不会陌生,至少会略有耳闻,但不会闻所未闻吧。大框架中关于虚拟DOM或多或少都有所涉及,然后接下来,我们就从原始码角度出发,看看Vue中的虚拟DOM时怎样的。

2.虚拟DOM简介

由于本系列文章是针对原始码深度Vue学习的,所以着重分析在Vue中对虚拟DOM是如何实现的,而对于虚拟DOM本身这个概念不做大篇幅的展开讨论,仅从以下几个问题简单介绍:

  1. 什么是虚拟DOM?

    所谓虚拟DOM,就是用一个JS对象来描述一个DOM节点,像如下示例:

 <div class="a" id="b">我是内容</div>

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

我们把其中DOM一个JS对象的必要东西通过一个对象表示出来,然后这个JS对象就可以使用描述这个DOM
异步,我们把这个JS对象就称为是这个真实DOM的虚拟DOM缓存。

  1. 为什么要有虚拟DOM?

    我们知道,Vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM,而操作真实DOM又是非常耗费性能的,这是因为浏览器的标准就把DOM设计的非常复杂,所以一个真正的DOM元素是非常庞大的,如下所示:

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

image

上图中我们打印一个简单的空div标签,就打印出这么多东西,更不用说复杂的,深层次的例程DOM了。直接可见,直接操作真实DOM是非常消耗性能的。

那么有没有什么解决方案呢?当然是有的。我们可以用JS的计算性能来换取操作DOM所消耗的性能。

既然我们逃不掉操作DOM这道坎,但是我们可以重置少的操作DOM。那如何在更新视图的时候重新少的操作DOM呢?最直观的思路就是我们不要盲目的去更新视图,或者通过对比数据变化前后的状态,计算出视图中某个地方需要更新,只更新需要更新的地方,而不需要更新的地方则不需关心,这样我们就可以允许少的操作DOM了。的用JS的计算性能来换取操作DOM的性能。

可以我们用JS模拟出一个DOM节点,虚拟称之为DOM节点。当数据发生变化时,对比我们变化前后的虚拟DOM节点,通过DOM-Diff算法计算出需要更新的地方,然后去更新需要更新的视图。

这就是虚拟产生DOM的原因以及最大的用途。

3. Vue中的虚拟DOM

前文我们介绍了虚拟DOM的概念以及为什么要有虚拟DOM,那么在Vue中虚拟DOM是怎么实现的呢?然后,我们从源码出发,深入学习一下。

3.1 VNode类

我们说了,虚拟DOM就是用JS来描述一个真实的DOM节点而在。Vue中就存在了一个VNode类,通过这个类,就我们可以实例化出不同类型的虚拟DOM节点,源码如下:

// 源码位置:src/core/vdom/vnode.js

export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    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 (): Component | void {
    return this.componentInstance
  }
}

从上面的代码中可以研磨:VNode类中包含了描述一个真实DOM例程所需要的各种属性,如tag表示解码器的标签名textchildren表示该中间包含的文本,表示该包含包含的子例程等。间不同的搭配,就可以描述出各种类型的真实摘要DOM

3.2 VNode的类型

上一小节最后我们说了,通过属性之间不同的搭配,VNode类可以描述出各种类型的真实基准DOM。那么它都可以描述出某种类型的例程呢?通过阅读代码,可以发现通过不同属性的搭配,可以描述出以下几种类型的例程。

  • 注释注释
  • 文字摘要
  • 元素推理
  • 组件例程
  • 函数式组件例程
  • 克隆议员

接下来,我们就把这几种类型的例程描述方式从二进制中一一对应起来。

3.2.1注释注释

注释描述符描述起来相对就非常简单了,它只是两个属性就够了,源码如下:

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

从上面的代码中可以看到,描述一个注释上面的两个属性,分别是:textisComment。其中text属性表示具体的注释信息,isComment是一个标志,用于标识一个队列是否是注释注释。

3.2.2文本基线

文本摘要描述起来比注释注释更简单,因为它只需要一个属性,那就是text属性,表示特定的文本信息。

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

3.2.3克隆例程

克隆副本就是把一个已经存在的副本复制副本出来,它主要是为了做模板编译优化时使用,这个后面我们会说到。

// 创建克隆节点
export function cloneVNode (vnode: VNode): 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
}

从上面的代码中可以看到,克隆指针就是把已有的属性的全部复制到新例程中,而现有例程和新克隆得到的例程之间唯一的不同就是克隆得到的例程isClonedtrue

3.2.4元素例程

在元素之下,元素例程更贴近于我们通常看到的真实DOM例程,它具有描述例程标签名词的tag属性,描述例程属性如classattributes等的data属性,具有描述包含的子例程信息的children属性等。所包含的情况相对而言比较复杂,源码中没有像前三种相同的直接写死(当然也不可能写死),那就举个简单的例子说明一下:

// 真实DOM节点
<div id='a'><span>难凉热血</span></div>

// VNode节点
{
  tag:'div',
  data:{},
  children:[
    {
      tag:'span',
      text:'难凉热血'
    }
  ]
}

我们可以看到,真实例程DOM中:div标签里面包含了一个span标签,而span标签里面有一段文本。反应到VNode例程上就如上所示:tag表示标签名,data表示标签的属性id等,children表示子数组。

3.2.5组件例程

组件上游除有元素索引具有的属性之外,它还有两个特有的属性:

  • componentOptions:组件的选项,如组件的props
  • componentInstance:当前组件例程对应的Vue实例

3.2.6函数式组件例程

函数式组件例程相较于组件例程,它又有两个特有的属性:

  • fnContext:函数式组件对应的Vue实例
  • fnOptions:组件的选项选项

3.2.7小结

以上就是VNode可以描述的多种多样的类型,它们本质上都是VNode类的实例,只是在实例化的时候引发的属性参数不同而已。

3.3 VNode的作用

说了这么多,那么VNodeVue的整个虚拟DOM过程起了什么作用呢?

其实VNode是作用是相当大的。我们在视图渲染之前,把写好的template模板先编译成VNode并缓存下来,等到数据发生变化页面需要重新渲染的时候,我们把数据发生变化后生成的VNode与前一次缓存下来的VNode进行对比,发现差异,然后有差异的对应VNode的真实DOM前缀就是需要重新渲染的例程,最后根据有差异的VNode创建出真实的DOM变量再插入到视图中,最终完成一次视图更新。

4.总结

本章首先介绍了虚拟DOM的一些基本概念和为什么要有虚拟DOM,其实说白了就是以JS的计算性能来换取操作真实DOM所消耗的性能。然后从原始码角度我们知道了在Vue中是通过VNode类来实例化出不同类型的虚拟DOM例程,并且学习了不同类型生成的属性的不同,所谓的不同类型的转换器其本质还是一样的,都是VNode类的实例,只是在实例化时替代的属性参数不同罢了。最后探究了VNode的作用,有了数据变化前后的VNode,我们才能进行后续的发现DOM-Diff差异,最终做到只更新有差异的观点,从而达到减少较少的操作真实DOM的目的,以节省性能。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容