vue--虚拟DOM

虚拟 DOM

虚拟DOM是什么?

虚拟 DOM 就是用一个 JS 对象来描述一个 DOM 节点。

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

{
    tag: 'div',
    attrs: {
        class: 'a',
        id: 'b'
    },
    text: '我是内容',
    children: []
}

Vue为什么采用虚拟DOM,而不直接操作真实DOM呢?

Vue是数据驱动视图,数据发生变化视图就要更新视图,但是操作 DOM 代价过于昂贵,可以用 js 的计算来换取操作 DOM 所消耗的性能,通过对比数据变化前后的状态,计算出视图中哪些地方需要更新,只要更新需要更新的地方。这样就可以尽少的操作 DOM 了。

通过利用 JS 模拟一个 DOM 节点,当数据发生变化时,对比变化前后的虚拟 DOM,通过 diff 算法计算出需要更新的地方,然后去更新视图。

VNode类

// src/core/vdom/vnode.js

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
    // 当前节点的子节点,是一个数组
    this.children = children
    // 当前节点的文本
    this.text = text
    // 当前虚拟节点对应的真实 dom 节点
    this.elm = elm
    // 当前节点的名字空间
    this.ns = undefined
    // 当前组件节点对应的 Vue 实例
    this.context = context
    // 函数式组件对应的 Vue 实例
    this.fnContext = undefined
    //
    this.fnOptions = undefined
    this.fnScopeId = undefined
    // 节点的 key 属性,被当作节点的标志,用以优化
    this.key = data && data.key
    // 组件的 option 选项
    this.componentOptions = componentOptions
    // 当前节点对应的组件的实例
    this.componentInstance = undefined
    // 当前节点的父节点
    this.parent = undefined
    // 是否为原生 HTML 或只是普通文本。
    this.raw = false
    // 静态节点标志
    this.isStatic = false
    // 是否作为根节点标志
    this.isRootInsert = true
    // 是否为注释节点
    this.isComment = false
    // 是否为克隆节点
    this.isCloned = false
    // 是否有 v-once 指令
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
}

VNode 中包含了一个真实的 DOM 节点所需要的属性,通过 VNode 类,来实例化出各种类型的真实 DOM 节点。

// 创建注释节点
const createEmptyVNode = (text: string = '') => {
  // new VNode 产生一个实例
  const node = new VNode()
  // 具体的注释信息
  node.text = text
  // 是注释节点的一个标志
  node.isComment = true
  // 返回这个产生的节点
  return node
}


// 创建文本节点
function createTextVNode (val: string | number) {
  // 只需要一个 text 节点。
  return new VNode(undefined, undefined, undefined, String(val))
}



// 克隆节点 (把一个已经存在的节点复制一份),做模版编译优化时使用。
function cloneVNode (vnode: VNode): VNode {
  // 就是把已有的节点的属性全部复制到新节点中
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    // #7975
    // clone children array to avoid mutating original in case of cloning
    // a child.
    vnode.children && vnode.children.slice(),
    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
  // 唯一不同就是新节点的 isClone 为 true
  cloned.isCloned = true
  return cloned
}

创建好虚拟节点之后,就到了比较阶段了.通过比较新旧两份 VNode,找出差异,然后更新差异的 DOM 节点。

patch

DOM-Diff 的过程就是 patch 过程。数据变化之后对应的虚拟 DOM(newVNode)。以 newVNode 为基准,对比 oldVNode. 从而让新旧 VNode 相同。 这就是 patch 过程干的事情。

  1. 创建节点
    如果 newVNode 中存在,oldVNode 中不存在,就去创建节点加入到 oldVNode.
function createElm () {
    if (isDef(tag)) {
        
    } else if (isTrue(vnode.isComment)) {

    } else {

    }
}

如果有 tag 标签,就是元素节点。调用 createElement 创建元素节点。如果有子节点就遍历创建所有子节点,将创建好的子节点插入到当前元素节点里面,最后插入到 DOM 中。

如果有 isComment 就代表是注释标签,调用 createComment 创建注释节点。在插入到 DOM.

如果都不是,就是文本节点,调用 createTextNode 创建文本节点。

  1. 删除节点
    如果 newVNode 中不存在, oldVNode 中存在,就去从 oldVNode 中删除这个节点。
function removeNode (el) {
    const parent = nodeOps.parentNode(el) 
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el)
    }
}

就是获取到要删除元素的父节点,然后调用 removeChild 方法。

  1. 更新节点
    如果 newVNode 和 oldVNode 都有,就以新的 VNode 为准,更新旧的 oldVNode。
 /**
   * 情况0: VNode 和 oldVNode 一样,退出程序。
   * 情况一:VNode 和 oldVNode 都是静态节点
   * 情况二:VNode 是文本节点
   *       - oldVNode 是文本节点,比较文本,不同则把 oldVNode 里的文本改成根 VNode 的文本一样。
   *       - oldVNode 不是文本节点,不管是什么,直接调用 setTextNode 方法该问文本节点,并且文本内容也和 VNode 一样。
   * 情况三:VNode 是元素节点
   *       - 该节点不包含子节点
   *            不包含子节点,也不是文本节点,说明是一个空节点。不管旧的节点里面是啥,直接清空。
   *       - 该节点包含子节点
   *            - 看旧的节点是否包含子节点,如果旧的节点里也包含子节点,递归对比更新子节点。
   *            - 旧的节点不包含子节点,这个旧节点可能是空节点或者是文本节点。
   *                - 如果旧的节点是空节点,把新的节点里的子节点创建一份插入到旧的节点。
   *                - 如果旧的节点是文本节点,文本清空,把新的节点里的子节点创建一份插入到旧的节点里面。
   * 
   */

function patchVnode () {
    // vnode 的子节点与 oldVnode 的子节点是否都存在
    if (isDef(oldCh) && isDef(ch)) {
        // 判断子节点是否相同,不同则更新子节点
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
}

  1. 总结
    patchVnode 方法,是复用同一个 dom 元素,如果新旧两个 VNode 对象都有子元素。应该调用 updateChildren 方法。

更新子节点 updateChildren

VNode 是元素节点,并且该节点包含子节点,oldVNode 也包含子节点,递归对比更新子节点



会找到对应的 oldVNode 和 newVNode 的第一个和最后一个索引作为指针,比较的过程就是移动指针。

然后找到对应的 newVNode 和 oldVNode 的第一个和最后一个 VNode 节点。

while 循环在新旧节点结束后停止,否则会不断的执行循环流程。

  1. 如果 oldStartVnode 为定义,就移动 oldCh 数组遍历的起始指针到后一位。

  2. 如果 oldEndVnode 为定义,则 oldCh 数组向前移动一位。

  3. newChildren 数组里的所有未处理子节点的第一个子节点和 oldChildren 数组里的所有未处理子节点的第一个比较,如果相同,就直接进入
    直接进入更新节点的操作,也无需进行节点移动操作。

  4. newChildren 数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,
    如果相同,那就直接进入更新节点的操作,无需进行节点移动操作.

  5. 把newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作
    更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;

  6. 把newChildren数组里所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作
    更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;

  7. 如果上述六种情况都不满足. 会走到默认 else。
    通过遍历 oldCh 数组,找出其中 key 的对象,并以 key 为键,索引为 value, 生成新的对象 oldKeyToIdx.

然后查看 newStartVnode 是否有 key 值,并查找 oldKeyToIdx 是否有相同的 key。

如果 newStartVnode 没有 key 或 oldKeyToIdx 没有有相同的 key。 就新增节点插入到 oldChildren 厘米为处理节点之前。

如果找到了,并且两个节点相同,说明可以复用,调用patchVnode方法复用dom元素并递归比较子元素,重置 oldCh 中相对的元素为 undefined.
然后插入到 oldStartVnode.elm 签名。newCh 的起始索引后移一位。

如果节点不同,就调用 createElm 新增元素,newCh 索引后移一位。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值