Vue源码解析:虚拟DOM加Differ算法之一锤定音(一)

之前我们讲过,Vue的特点之一是数据驱动视图,那么今天我们来讲讲Vue的另一特点虚拟dom。

虚拟Dom基础

1、Vue的虚拟dom是什么?

简单来讲,Vue的虚拟dom就是一个JavaScript对象,用对象的方式去描述dom的结构。

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

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

2、为什么要使用虚拟dom?

我们知道浏览器将页面的dom设计的非常复杂,而Vue的特点之一是数据驱动视图,换言之,也就是说,vue是通过数据的变化来驱动视图的更新,也就意味着是数据变化对dom的操作非常的频繁,这样的代价是非常大,但是我们又逃不多对dom的操作,那么怎么办呢?其实很简单,我们可以在数据变化前后生成俩分dom,然后通过diff算法将这俩分dom做比较,看看他们有什么不一样的,将不一样的dome更新到真实的dom上,是不是就大大就减少了真实的dom的操作,其实就是我们所说的虚拟dom.

3、Vue中如何实现的虚拟dom?

那么vue中是如何实现的虚拟dom呢?其实也很简单,就是通过vnode这样一个类实现,直接上源码:

// 源码位置: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属性。

4、VNode的类型:

  • 注释节点
  • 文本节点
  • 元素节点
  • 组件节点
  • 函数式组件节点
  • 克隆节点

Diff算法

提到dom,diff算法就是被紧紧的联系在一起,那么到底什么是diff算法呢,在vue中把diff算法称之为patch的过程,patch的意思就是打补丁,所以,就是这样,你懂了吧。那么这个patch的过程是怎么样的呢?我们在远古时代就知道。对于dom的操作无非就是增、删、改、查这四个字,那么vue的patch的过程其实就是VNode的增、删、改的过程,所以首先要讲清楚patch的对象是什么?那就是,每次数据变化之前的虚拟VNode,和数据变化之后的虚拟VNode,以新的VNode为基准,改造旧的oldVNode使之成为跟新的VNode一样,这就是patch过程要干的事。

增:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。

删:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。

改:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode

(1)VNode的创建

上面我们讲到VNode的类型有六种,但是实际其实只能有三种节点可以被创建,即:文本节点、注释节点、元素节点:

// 源码位置: /src/core/vdom/patch.js
function createElm (vnode, parentElm, refElm) {
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      	vnode.elm = nodeOps.createElement(tag, vnode)   // 创建元素节点
        createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
        insert(parentElm, vnode.elm, refElm)       // 插入到DOM中
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)  // 创建注释节点
      insert(parentElm, vnode.elm, refElm)           // 插入到DOM中
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)  // 创建文本节点
      insert(parentElm, vnode.elm, refElm)           // 插入到DOM中
    }
  }

如果判断有tag标签则被认为是元素节点,调用createElement()方法,创建元素节点;

如果判断isComment是true,调用createComment()创建注释节点;

如果以上俩种都不是,那么就被认为是文本节点,调用createTextNode节点。

最后创建完成插入dom中。

(2)VNode的删除

当发现新的VNode中没有,而旧的VNode中有,那也就意味着要删除旧的VNode中没有的部分。

function removeNode (el) {
    const parent = nodeOps.parentNode(el)  // 获取父节点
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el)  // 调用父节点的removeChild方法
    }
  }

(3)VNode的更新 

VNode的更新是相对上俩个比较复杂一点的,那具体是什么情况呢?且听我慢慢道来:

首先,讲一下静态节点,就是从最初创建开始,无论数据怎么变化都不会变化的节点叫做静态节点,那么如果新旧俩种都是静态节点,那么直接跳过,因为不会有变化;

其次,如果是文本节点,那么就要看旧的VNode是不是纯文本,如果是,那就直接比对VNode内的纯文本,然后直接更新;如果不是纯文本,那么直接调用setNextNode改成和新的一样的VNode即可。

最后,如果是元素节点,如果这个元素节点没有子节点,而且又不是文本节点,那就说明这是个空节点,直接把旧的VNode清空。如果旧VNode中有子节点,那么就采用递归的方式,重新判断,达到和新的VNode一样的节点。

// 更新节点
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  // vnode与oldVnode是否完全一样?若是,退出程序
  if (oldVnode === vnode) {
    return
  }
  const elm = vnode.elm = oldVnode.elm

  // vnode与oldVnode是否都是静态节点?若是,退出程序
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    return
  }

  const oldCh = oldVnode.children
  const ch = vnode.children
  // vnode有text属性?若没有:
  if (isUndef(vnode.text)) {
    // vnode的子节点与oldVnode的子节点是否都存在?
    if (isDef(oldCh) && isDef(ch)) {
      // 若都存在,判断子节点是否相同,不同则更新子节点
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
    // 若只有vnode的子节点存在
    else if (isDef(ch)) {
      /**
       * 判断oldVnode是否有文本?
       * 若没有,则把vnode的子节点添加到真实DOM中
       * 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
       */
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    }
    // 若只有oldnode的子节点存在
    else if (isDef(oldCh)) {
      // 清空DOM中的子节点
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
    // 若vnode和oldnode都没有子节点,但是oldnode中有文本
    else if (isDef(oldVnode.text)) {
      // 清空oldnode文本
      nodeOps.setTextContent(elm, '')
    }
    // 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
  }
  // 若有,vnode的text属性与oldVnode的text属性是否相同?
  else if (oldVnode.text !== vnode.text) {
    // 若不相同:则用vnode的text替换真实DOM的文本
    nodeOps.setTextContent(elm, vnode.text)
  }
}

这里我们只是讲了虚拟dom和diff算法的大致思路,具体的内容,我们将在下一节中讲授。敬请期待。。。。 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值